HAP发货单管理Demo


title: HAP发货单管理Demo
categories: 后端
tags:

  • hap

整体效果

前端效果

发货退货单据界面:

前端效果模板1.png

头行查询出现的属性:头:1.单号 2.发货时间 3.单据类型 4.仓库 5.接收仓库 6.发货单状态 7.司机姓名 8.司机电话 9.车牌号码 10.运费 11.备注 12.创建人

单号:DOC_NUMBER,必输,编号规则:D+yyyyMMdd+(1-1000随机整数,不够4位用0补齐) 
发货时间:大于当前日期,必输
单据类型:DOC_TYPE,ISSUED/RETURN,下拉列表
仓库:ORGANIZATION_ID,LOV,必输,从xxfnd_organization_access_b表中获取,根据当前角色来筛选组织 e.param['roleId'] =("${Session.roleId}");
接收仓库:TO_ORGANIZATION_ID,LOV,当DOC_TYPE=ISSUED时,必输,不可等于来源仓库,否则灰掉不可录入
发货状态:显示LOOKUP的MEANING                   
运费:只录入数字,ISSUED类型时必输

行:1.行号 2.物料编码 3.是否批次控制 4.物料说明 5.批次 6.行数量 7.待发货数量 8.已发货数量

物料编码:LOV,字段对应为INVENTORY_ITEM_ID,根据头上的ORGANIZATION_ID作为筛选条件                          
是否批次控制:只读,CHECKBOX,由"物料编码LOV"带出,物料TABLE:xxinv_material_item                 
物料说明:只读,由"物料编码LOV"带出
批次:IF(LOT_CONTROL=Y)要录入, 否则不用录入
行数量:"PENDING"时必输,其他状态只读显示
待发货数量:"PENDING,ISSUED"时只读,“COMFIRMED”默认(行数量-已发货数量)
已发货数量(ISSUED_QTY):只读根据xxinv_material_txns的SOURCE_TABLE,SOURCE_KEY1来汇总行的已发货数量

单据查询界面:

搜索条件:1.单号 2.仓库 3.单据状态 4.物料编码 5.是否存在未发货行 6.单据类型

查询结果:1.单据号 2.单据类型 3.单据状态 4.仓库 5.目标仓库 6.司机 7.创建人 (编辑与查看按钮)

前端效果模板2.png

前端界面对应逻辑

头行界面逻辑:

头行界面逻辑.png

单据查询界面逻辑:

单据查询界面逻辑.png

编码描述与lov值准备:

lov与编码描述准备.png

开发

数据库准备

创建数据库

创建数据库hap4_demo,把权限赋给hap_dev用户
因为我们已经有了hap_dev用户,这里就只是创建数据库和刷新权限了。

CREATE SCHEMA hap4_ship_order DEFAULT CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON hap4_ship_order.* TO hap_dev@'%';
FLUSH PRIVILEGES;

修改pom文件中的依赖与前端config.js

修改pom文件中的依赖

<dependency>
            <groupId>io.choerodon</groupId>
            <artifactId>hap-core</artifactId>
        </dependency>
        <dependency>
            <groupId>io.choerodon</groupId>
            <artifactId>hap-security-standard</artifactId>
        </dependency>

修改config.js

 modules: [
    '../target/generate-react/choerodon-fnd-util',
    '../target/generate-react/hap-core',
  ],

初始化数据库表

修改initdatabse.sh脚本中的数据库名称和target.jar的名称与项目名称一致
再执行该脚本sh init-database.sh

修改xlsx文件

将grid的demo路径修改为项目路径。就是将hap-demo替换为这里的ship-order。修改这里是因为前端路径的问题,路由的js中有${match.url}匹配的就是当前项目名,xlsx中写死了hap-demo,需要手动修改。

打包并运行

执行如下命令脚本

mvn clean package
npm install --registry https://nexus.choerodon.com.cn/repository/choerodon-npm/
npm run build
mvn spring-boot:run

所需表初始化

添加groovy建表的脚本,这里给出一个实例:物料表。

package script.db

databaseChangeLog(logicalFilePath: "hap4-ship-order.groovy") {

    changeSet(author: "lth", id: "2019-08-26-hap4_ship_order") {
        if (helper.dbType().isSupportSequence()) {
            createSequence(sequenceName: 'XXINV_MATERIAL_ITEM_S', startValue: "100")
        }
        createTable(tableName: "XXINV_MATERIAL_ITEM", remarks: "物料信息表") {
            if (helper.dbType().isSupportAutoIncrement()) {
                column(name: "ITEM_ID", type: "BIGINT", autoIncrement: "true", startWith: "10001", remarks: "PK") {
                    constraints(nullable: "false", primaryKey: "true", primaryKeyName: "XXINV_MATERIAL_ITEM_PK")
                }
            } else {
                column(name: "ITEM_ID", type: "BIGINT", remarks: "PK") {
                    constraints(nullable: "false", primaryKey: "true", primaryKeyName: "XXINV_MATERIAL_ITEM_PK")
                }
            }
            column(name: "INVENTORY_ITEM_ID", type: "BIGINT(20)", remarks: "ERP物料ID") {
                constraints(nullable: "false")
            }
            column(name: "ITEM_NUMBER", type: "VARCHAR(60)", remarks: "物料编码") {
                constraints(nullable: "false")
            }
            column(name: "ORGANIZATION_ID", type: "BIGINT(20)", remarks: "库存组织ID") {
                constraints(nullable: "false")
            }
            column(name: "DESCRIPTION", type: "VARCHAR(240)", remarks: "物料说明")
            column(name: "LOT_CONTROL", type: "VARCHAR(10)", remarks: "是否批次控制")
            column(name: "UOM_CODE", type: "VARCHAR(30)", remarks: "单位") {
                constraints(nullable: "false")
            }
            column(name: "ITEM_STATUS", type: "VARCHAR(30)", remarks: "物料状态 ACTIVE/INACTIVE") {
                constraints(nullable: "false")
            }

            //必输字段
            column(name: "OBJECT_VERSION_NUMBER", type: "BIGINT", defaultValue: "1")
            column(name: "REQUEST_ID", type: "bigint", defaultValue: "-1")
            column(name: "PROGRAM_APPLICATION_ID", type: "BIGINT(11)")
            column(name: "PROGRAM_ID", type: "bigint", defaultValue: "-1")
            column(name: "PROGRAM_UPDATE_DATE", type: "DATE")
            column(name: "CREATED_BY", type: "bigint", defaultValue: "-1")
            column(name: "CREATION_DATE", type: "datetime", defaultValueComputed: "CURRENT_TIMESTAMP")
            column(name: "LAST_UPDATED_BY", type: "bigint", defaultValue: "-1")
            column(name: "LAST_UPDATE_DATE", type: "datetime", defaultValueComputed: "CURRENT_TIMESTAMP")
            column(name: "LAST_UPDATE_LOGIN", type: "bigint", defaultValue: "-1")

            column(name: "ATTRIBUTE_CATEGORY", type: "varchar(30)")
            column(name: "ATTRIBUTE1", type: "varchar(240)")
            column(name: "ATTRIBUTE2", type: "varchar(240)")
            column(name: "ATTRIBUTE3", type: "varchar(240)")
            column(name: "ATTRIBUTE4", type: "varchar(240)")
            column(name: "ATTRIBUTE5", type: "varchar(240)")
            column(name: "ATTRIBUTE6", type: "varchar(240)")
            column(name: "ATTRIBUTE7", type: "varchar(240)")
            column(name: "ATTRIBUTE8", type: "varchar(240)")
            column(name: "ATTRIBUTE9", type: "varchar(240)")
            column(name: "ATTRIBUTE10", type: "varchar(240)")
            column(name: "ATTRIBUTE11", type: "varchar(240)")
            column(name: "ATTRIBUTE12", type: "varchar(240)")
            column(name: "ATTRIBUTE13", type: "varchar(240)")
            column(name: "ATTRIBUTE14", type: "varchar(240)")
            column(name: "ATTRIBUTE15", type: "varchar(240)")
        }
        //判断是不是postgresql数据库,并且添加唯一约束。我们是mysql数据库,
        if (!helper.isPostgresql()) {
            addUniqueConstraint(columnNames: "INVENTORY_ITEM_ID,ORGANIZATION_ID", tableName: "XXINV_MATERIAL_ITEM", constraintName: "XXINV_MATERIAL_ITEM_U1")
        } else {
            addUniqueConstraint(columnNames: "INVENTORY_ITEM_ID", tableName: "XXINV_MATERIAL_ITEM", constraintName: "FXXINV_MATERIAL_ITEM_U1")
        }
    }
}

顺便提一句:创建索引的方法如下:

createIndex(tableName: "XXINV_SHIPED_DOC_LINES", indexName: "XXINV_SHIPED_DOC_LINES_N1") {column(name: "INVENTORY_ITEM_ID", type: "BIGINT(20)");column(name: "ORGANIZATION_ID",type: "BIGINT(20)")}

建好groovy脚本之后,如果之前运行过initdatabase脚本,需要删除数据库表databasechangelog中之前的操作记录,否则可能会脚本运行之后没有插入数据库。

后端开发

建立对应的mapper dto service 包,然后创建实体类 mapper类 service类。这些上一篇的demo开发中有解释实体类中注解的使用和这些类或者接口应该继承或者实现什么类或者接口,这里就不再赘述了。

代码维护状态创建和lov的创建

代码维护创建:在后台的代码维护页面创建相应的Lookup Type。

lov与编码描述准备.png

lov创建

首先我们知道,在后端定义lov时会指定一个sql.id,这个东西指的就是我们mapper接口名+对应方法。当我们在后台调用值集框时,就会自动创建一个mapper对象。
不过仅仅这样还不够,mapper中方法的定义很有讲究。

我这里拿物料来举例,这个接口里面有两个lov的查询。发现有没有特别之处?最特殊的就是其中的参数,传递的是一个完整的物料对象。这个对象中有我们的筛选条件。这里就是尤其要注意的地方。

public interface MaterialItemMapper extends Mapper<MaterialItem> {

    /**
     * 通过仓库id模糊查询出仓库(这里没有仓库表)
     * @param materialItem
     * @return
     */
    List<MaterialItem> selectOrganizationLov(MaterialItem materialItem);

    /**
     * 通过头的仓库Id(不用手动输入)找对应的物料,描述作为再筛选的条件(需要手动输入)
     * @param materialItem
     * @return
     */
    List<MaterialItem> selectMaterialItemLov(MaterialItem materialItem);
    
}

现在在后台创建对应的描述:注意我在图中的解释,这里就不再拿出来叙述了,在图上解释更为直观。

lov的定义.png

物料对应的mapper.xml文件:上面的参数是对象,在xml中的写法又与以往的写法要变化一些。

  1. ResultMap中需要继承io.choerodon.mybatis.mapper.StdMapper.STD
  2. 写的查询方法标签中需要指定resultMap。不能用resultType来代替resultMap的定义,而且还要指定parameterType来指定方法传来的参数。
  3. select的参数需要指定,不能用*来代替。
  4. 如果只有一个if标签,也需要用<\where>来包裹。后台值集查询时,会先执行一个查询总数的sql,如果只用where会使动态生成的sql多一个where,会失败。
<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="hap.demo.core.order.mapper.MaterialItemMapper">


    <resultMap id="baseMaterial" type="hap.demo.core.order.dto.MaterialItem" extends="io.choerodon.mybatis.mapper.StdMapper.STD">
        <id property="itemId" column="ITEM_ID" jdbcType="DECIMAL"/>
        <result property="inventoryItemId" column="INVENTORY_ITEM_ID" jdbcType="DECIMAL"/>
        <result property="itemNumber" column="ITEM_NUMBER" jdbcType="VARCHAR"/>
        <result property="organizationId" column="ORGANIZATION_ID" jdbcType="DECIMAL"/>
        <result property="description" column="DESCRIPTION" jdbcType="VARCHAR"/>
        <result property="lotControl" column="LOT_CONTROL" jdbcType="VARCHAR"/>
        <result property="uomCode" column="UOM_CODE" jdbcType="VARCHAR"/>
        <result property="itemStatus" column="ITEM_STATUS" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="selectOrganizationLov" resultMap="baseMaterial" parameterType="hap.demo.core.order.dto.MaterialItem">
        SELECT distinct item.ORGANIZATION_ID FROM
        xxinv_material_item item
        <where>
            <if test="organizationId != null">
                <bind name="pattern" value="'%' + organizationId + '%'"/>
                item.ORGANIZATION_ID LIKE #{pattern}
            </if>
        </where>
    </select>

    <select id="selectMaterialItemLov" resultMap="baseMaterial" parameterType="hap.demo.core.order.dto.MaterialItem">
        SELECT item.INVENTORY_ITEM_ID,item.ITEM_NUMBER,item.DESCRIPTION,item.ORGANIZATION_ID
        FROM xxinv_material_item item
        <where>
            <if test="organizationId != null">
                item.ORGANIZATION_ID = #{organizationId}
            </if>
            <if test="description != null">
                <bind name="pattern" value="'%' + description + '%'"/>
                and item.DESCRIPTION LIKE #{pattern}
            </if>
        </where>
    </select>
</mapper>

订单头和行查询 (增删改也补充上了)

头:创建VO类,整理sql语句

先写出条件查询订单头,因为有不是头表中的列作为条件,所以我使用了一个OrderHeaderVO来包裹条件字段和结果字段。注意其中的shippedTime属性,它来自ShipDoc类。①这个属性在name为OrderHeader的DataSet中,对应字段的type是datetime类型。②shippedTime的java类型是java.util.Date。③OrderHeaderVO的resultMapper中shippedTime对应的jdbctype为TIMESTAPE,对应的javatype是java.util.Date。还有这里继承了BaseDTO是因为前端的增删改查操作会传一个status值,只有继承了BaseDTO之后,在写service的实现类并重写方法时,才能拿到状态值,进行对应的操作。

这里需要注意数据中的字段是datetime类型的,讲道理对应的java中类型为TIMESTAPE,那么jdbctype是它可以理解;那为啥实体类的字段不使用TIMESTAPE而使用Date呢?通过实践,如果使用TIMESTAPE,前端的DataSet转化为OrderHeaderVO就会报错,不能转化成TIMESTAPE;如果使用@JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss")能够转化成对象了。不过我看BaseDTO的源码中,它的创建与更新时间属性都是java.util.Date类型,数据库中也是datetime类型,在mapper.xml中用的是jdbctype="TIMESTAPE";这就省略了使用注解。这里两种方式应该都是能行的。我当时时间查询之后没有在前端显示出来是因为我特么select语句的时间字段没有对应上resultMap中的列,真是难受。

public class OrderHeaderVO extends BaseDTO {

    private Long shipDocId;
    //结果字段

    private String docNumber;

    private String docType;

    private String shippmentStatus;

    private Long organizationId;

    private Long toOrganizationId;

    private String driverName;

    private String driverPhone;

    private Double freight;

    private String memo;

    private Date shippedTime;

    private Long createdBy;


    //条件字段

    private Long inventoryItemId;

    private String issuedFlag;
    
    private List<OrderLineVO> lines;
    //省略getter和setter
}

下面给出基本的连接,没有添加条件。这条sql我改了三次,第一次select的值只写了头界面展示的;第二次由于行上需要展示头界面未包含的,就添加了隐藏的创建时间等字段;第三次由于是左连接,如果一个头对应多个行,那么头就会显示多次,为了不重复显示头就添加了DISTINCT。

SELECT DISTINCT
    doc.SHIP_DOC_ID,
    doc.DOC_NUMBER,
    doc.DOC_TYPE,
    doc.SHIPPMENT_STATUS,
    doc.ORGANIZATION_ID,
    doc.TO_ORGANIZATION_ID,
    doc.DRIVER_NAME,
    doc.DRIVER_PHONE,
    doc.FREIGHT,
    doc.MEMO,
    doc.CREATED_BY,
    doc.SHIPPED_TIME
FROM
    xxinv_shiped_doc doc
LEFT JOIN xxinv_shiped_doc_lines line ON (
    doc.SHIP_DOC_ID = line.SHIP_DOC_ID
)
LEFT JOIN xxinv_material_item item ON (
    line.INVENTORY_ITEM_ID = item.INVENTORY_ITEM_ID
    AND line.ORGANIZATION_ID = item.ORGANIZATION_ID
)

对应的mybatis中的语句:

<resultMap id="baseOrderHeader" type="hap.demo.core.order.dto.vo.OrderHeaderVO">
        <result property="shipDocId" column="SHIP_DOC_ID" jdbcType="DECIMAL"/>
        <result property="createdBy" column="CREATED_BY" jdbcType="DECIMAL"/>
        <result property="docNumber" column="DOC_NUMBER" jdbcType="VARCHAR"/>
        <result property="docType" column="DOC_TYPE" jdbcType="VARCHAR"/>
        <result property="driverName" column="DRIVER_NAME" jdbcType="VARCHAR"/>
        <result property="driverPhone" column="DRIVER_PHONE" jdbcType="VARCHAR"/>
        <result property="inventoryItemId" column="INVENTORY_ITEM_ID" jdbcType="DECIMAL"/>
        <result property="issuedFlag" column="ISSUED_FLAG" jdbcType="VARCHAR"/>
        <result property="organizationId" column="ORGANIZATION_ID" jdbcType="DECIMAL"/>
        <result property="shippmentStatus" column="SHIPPMENT_STATUS" jdbcType="VARCHAR"/>
        <result property="toOrganizationId" column="TO_ORGANIZATION_ID" jdbcType="DECIMAL"/>
        <result property="freight" column="FREIGHT" jdbcType="DECIMAL"/>
        <result property="memo" column="MEMO" jdbcType="VARCHAR"/>
        <result property="shippedTime" column="SHIPPED_TIME" javaType="java.util.Date" jdbcType="TIMESTAMP"/>
        <collection property="lines" javaType="ArrayList" column="shipDocId" ofType="hap.demo.core.order.dto.vo.OrderLineVO"
                    select="hap.demo.core.order.mapper.ShipDocLinesMapper.selectOrderLine"/>
    </resultMap>
     <select id="selectOrderHeader" resultMap="baseOrderHeader" >
        SELECT DISTINCT
            doc.SHIP_DOC_ID,doc.DOC_NUMBER,doc.DOC_TYPE,doc.SHIPPMENT_STATUS,doc.ORGANIZATION_ID,
        doc.TO_ORGANIZATION_ID,doc.DRIVER_NAME,doc.DRIVER_PHONE,doc.FREIGHT,doc.MEMO,doc.CREATED_BY,doc.SHIPPED_TIME
        FROM
            xxinv_shiped_doc doc
        LEFT JOIN xxinv_shiped_doc_lines line ON (
            doc.SHIP_DOC_ID = line.SHIP_DOC_ID
        )
        LEFT JOIN xxinv_material_item item ON (
            line.INVENTORY_ITEM_ID = item.INVENTORY_ITEM_ID
            AND line.ORGANIZATION_ID = item.ORGANIZATION_ID
        )
        <where>
            <if test="orderHeaderVO.docNumber != null">
                doc.DOC_NUMBER =#{orderHeaderVO.docNumber}
            </if>
            <if test="orderHeaderVO.docType != null">
                AND doc.DOC_TYPE = #{orderHeaderVO.docType}
            </if>
            <if test="orderHeaderVO.shippmentStatus != null">
                AND doc.shippment_status =#{orderHeaderVO.shippmentStatus}
            </if>
            <if test="orderHeaderVO.organizationId != null">
                AND doc.organization_id =#{orderHeaderVO.organizationId}
            </if>
            <if test="orderHeaderVO.inventoryItemId != null">
                AND item.INVENTORY_ITEM_ID =#{orderHeaderVO.inventoryItemId}
            </if>
            <if test="orderHeaderVO.issuedFlag != null">
                AND line.ISSUED_FLAG  =#{orderHeaderVO.issuedFlag}
            </if>
        </where>
    </select>

行:对应的VO类和sql语句

创建行OrderLineVO类,主要就是包含在前端显示的字段,或者是需要行实体类需要用到的字段。 上面头的实体中之所以要加行的list,是因为,之后新建头同时插入行 或者有头但 更新行的时候,能够从重写的mutations方法中的headerList参数中,header对象中能拿到对应的行list,从而拿到行数据。
下面加了一个@JsonIgnoreProperties(ignoreUnknown = true)注解是因为前端的行DataSet转化为行实体的时候会把当中定义的lov的对象也传进来。这个注解能让不是实体中的字段被忽略。

@JsonIgnoreProperties(ignoreUnknown = true)
public class OrderLineVO extends BaseDTO {

    private Long lineNum;

    private Long inventoryItemId;

    private String itemNumber;
    //物料表中

    private String lotControl;

    private String description;

    //行表

    private String lotNumber;

    private Double lineQty;

    private Double issueReqQty;

    //已发货数量

    private Double shippedQty;
    //省略getter和setter
}

前端行对应的mybatis中的sql语句

<resultMap id="baseOrderLineVO" type="hap.demo.core.order.dto.vo.OrderLineVO">
        <result property="description" column="DESCRIPTION" jdbcType="VARCHAR"/>
        <result property="lineNum" column="LINE_NUM" jdbcType="DECIMAL"/>
        <result property="inventoryItemId" column="INVENTORY_ITEM_ID" jdbcType="DECIMAL"/>
        <result property="itemNumber" column="ITEM_NUMBER" jdbcType="VARCHAR"/>
        <result property="lotControl" column="LOT_CONTROL" jdbcType="VARCHAR"/>
        <result property="lotNumber" column="LOT_NUMBER" jdbcType="VARCHAR"/>
        <result property="lineQty" column="LINE_QTY" jdbcType="DECIMAL"/>
        <result property="issueReqQty" column="ISSUE_REQ_QTY" jdbcType="DECIMAL"/>
    </resultMap>
    <select id="selectOrderLine" resultMap="baseOrderLineVO">
        SELECT
            line.LINE_NUM,
            item.ITEM_NUMBER,
            item.INVENTORY_ITEM_ID,
            item.LOT_CONTROL,
            item.DESCRIPTION,
            line.LOT_NUMBER,
            line.LINE_QTY,
            line.ISSUE_REQ_QTY
        FROM
            xxinv_shiped_doc_lines line
        LEFT JOIN xxinv_material_item item ON (
            line.INVENTORY_ITEM_ID = item.INVENTORY_ITEM_ID
            AND line.ORGANIZATION_ID = item.ORGANIZATION_ID
        )
        WHERE line.SHIP_DOC_ID=#{shipDocId}
    </select>

创建对应的Service和Impl类

Service类不说了,Impl类说一下:这个OrderHeaderVO没有对应的数据库表,那么就不用继承BaseServiceImpl了,Service类也不用继承BaseService类。不过还是需要实现IDatasetService接口的,这样前端的dataset的请求才能有对应的操作。
queries方法中的参数body中包裹的就是dataset.js中的query fields属性;前端编辑了哪些查询条件,把他的name与value作为body的k-v。例如前端,我前端有一个如下查询字段的代码,那么我如果在前端进行了物料lov的选择,随便选一个物料,点击查询;这个接收到的body中会包含了什么呢?看下面的图就知道了,会把这两个属性查到的值,和它的name作为键值对传入body。知道这个了,我后面通过shopDocId查行就能通过前端拿到shopDocId了。

{ name: 'materialItem4', type: 'object',textField: 'itemNumber', label: '物料编码', lovCode: 'XXINV_ITEMS' },
        { name: 'inventoryItemId', type: 'number', label: 'ERP物料ID', bind: 'materialItem4.inventoryItemId' },
queries中的body.png

下面是初步的查询的Service,并没有测试是否能增删改。这里面有很多错误。

@Service
@Transactional(rollbackFor = Exception.class)
@Dataset("OrderHeader")
public class OrdreHeaderServiceImpl implements OrderHeaderService, IDatasetService<OrderHeaderVO> {

    @Autowired
    private ShipDocMapper shipDocMapper;
    @Autowired
    private ShipDocLinesMapper shipDocLinesMapper;

    @Override
    public List<OrderHeaderVO> listOrderHeader(OrderHeaderVO orderHeaderVO) {
        return shipDocMapper.selectOrderHeader(orderHeaderVO);
    }

    @Override
    public List<?> queries(Map<String, Object> body, int page, int pageSize, String sortname, boolean isDesc) {
        try {
            OrderHeaderVO orderHeaderVO = new OrderHeaderVO();
            BeanUtils.populate(orderHeaderVO, body);
            PageHelper.startPage(page, pageSize);
            return shipDocMapper.selectOrderHeader(orderHeaderVO);
        } catch (Exception e) {
            throw new DatasetException("dataset.error", e);
        }
    }

    @Override
    public List<OrderHeaderVO> mutations(List<OrderHeaderVO> list) {
        for (OrderHeaderVO header : list) {
            switch (header.get__status()) {
                case ADD:
                    insertOrderHeader(header);
                    break;
                case DELETE:
                    deleteOrderHeader(header);
                    break;
                case UPDATE:
                    updateOrderHeader(header);
                    break;
                default:
                    break;
            }
        }
        return list;
    }


    private ShipDoc headerToShipDoc(OrderHeaderVO header) {
        return new ShipDoc(header.getOrganizationId(),
                header.getDocNumber(),
                header.getToOrganizationId(),
                header.getShippedTime(),
                header.getDocType(),
                header.getShippmentStatus(),
                header.getCreatedBy(),
                header.getDriverName(),
                header.getDriverPhone(),
                header.getFreight(),
                header.getMemo());


    }

    private ShipDocLines orderLineToShipDocLines(OrderHeaderVO header,OrderLineVO orderLine) {
        return new ShipDocLines(header.getShipDocId(),
                orderLine.getLineNum(),
                orderLine.getInventoryItemId(),
                header.getOrganizationId(),
                orderLine.getIssueReqQty(),
                orderLine.getLineQty(),
                orderLine.getLotNumber(),
                header.getIssuedFlag());
    }

    private void insertOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = shipDocLinesMapper.selectOrderLine(header.getShipDocId());
        ShipDoc shipDoc = headerToShipDoc(header);
        shipDocMapper.insert(shipDoc);
        if (orderLines.size() != 0) {
            for (OrderLineVO line:orderLines
            ) {
                ShipDocLines shipDocLine = orderLineToShipDocLines(header, line);
                shipDocLinesMapper.insert(shipDocLine);
            }
        }
    }

    private void deleteOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = shipDocLinesMapper.selectOrderLine(header.getShipDocId());
        ShipDoc shipDoc = headerToShipDoc(header);
        shipDocMapper.delete(shipDoc);
        if (orderLines.size() != 0) {
            for (OrderLineVO line:orderLines
            ) {
                ShipDocLines shipDocLine = orderLineToShipDocLines(header, line);
                shipDocLinesMapper.delete(shipDocLine);
            }
        }
    }

    private void updateOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = shipDocLinesMapper.selectOrderLine(header.getShipDocId());
        ShipDoc shipDoc = headerToShipDoc(header);
        shipDocMapper.updateByPrimaryKey(shipDoc);
        if (orderLines.size() != 0) {
            for (OrderLineVO line:orderLines
            ) {
                ShipDocLines shipDocLine = orderLineToShipDocLines(header, line);
                shipDocLinesMapper.updateByPrimaryKey(shipDocLine);
            }
        }
    }
}

下面给出比较完善的头的实现类:基本实现了单次插入头和行,或者在头的基础上修改或者插入行,也实现了头行一起删除。

@Service
@Transactional(rollbackFor = Exception.class)
@Dataset("OrderHeader")
public class OrdreHeaderServiceImpl implements OrderHeaderService, IDatasetService<OrderHeaderVO> {

    @Autowired
    private ShipDocMapper shipDocMapper;
    @Autowired
    private ShipDocLinesMapper shipDocLinesMapper;

    @Override
    public List<OrderHeaderVO> listOrderHeader(OrderHeaderVO orderHeaderVO) {
        return shipDocMapper.selectOrderHeader(orderHeaderVO);
    }

    @Override
    public List<?> queries(Map<String, Object> body, int page, int pageSize, String sortname, boolean isDesc) {
        try {
            OrderHeaderVO orderHeaderVO = new OrderHeaderVO();
            BeanUtils.populate(orderHeaderVO, body);
            PageHelper.startPage(page, pageSize);
            return shipDocMapper.selectOrderHeader(orderHeaderVO);
        } catch (Exception e) {
            throw new DatasetException("dataset.error", e);
        }
    }

    @Override
    public List<OrderHeaderVO> mutations(List<OrderHeaderVO> list) {
        for (OrderHeaderVO header : list) {
            switch (header.get__status()) {
                case ADD:
                    insertOrderHeader(header);
                    break;
                case DELETE:
                    deleteOrderHeader(header);
                    break;
                case UPDATE:
                    updateOrderHeader(header);
                    break;
                default:
                    break;
            }
        }
        return list;
    }

    /**
     * 将前端的header转化为订单头
     *
     * @param header
     * @return
     */
    private ShipDoc headerToShipDoc(OrderHeaderVO header) {
        ShipDoc frontShipDoc = new ShipDoc();
        frontShipDoc.setOrganizationId(header.getOrganizationId());
        frontShipDoc.setDocNumber(header.getDocNumber());
        frontShipDoc.setToOrganizationId(header.getToOrganizationId());
        frontShipDoc.setShippedTime(header.getShippedTime());
        frontShipDoc.setDocType(header.getDocType());
        frontShipDoc.setShippmentStatus(header.getShippmentStatus());
        frontShipDoc.setCreatedBy(header.getCreatedBy());
        frontShipDoc.setDriverName(header.getDriverName());
        frontShipDoc.setDriverPhone(header.getDriverPhone());
        frontShipDoc.setFreight(header.getFreight());
        frontShipDoc.setMemo(header.getMemo());

        return frontShipDoc;
    }

    /**
     * 将前端的line转化为订单行
     *
     * @param header
     * @param orderLine
     * @return
     */
    private ShipDocLines orderLineToShipDocLines(OrderHeaderVO header, OrderLineVO orderLine) {
        ShipDocLines frontShipDocLine = new ShipDocLines();

        frontShipDocLine.setShipDocId(header.getShipDocId());
        frontShipDocLine.setLineNum(orderLine.getLineNum());
        frontShipDocLine.setInventoryItemId(orderLine.getInventoryItemId());
        frontShipDocLine.setOrganizationId(header.getOrganizationId());
        frontShipDocLine.setIssueReqQty(orderLine.getIssueReqQty());
        frontShipDocLine.setLineQty(orderLine.getLineQty());
        frontShipDocLine.setLotNumber(orderLine.getLotNumber());
        frontShipDocLine.setIssuedFlag(header.getIssuedFlag());

        return frontShipDocLine;
    }

    private void insertOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = header.getLines();
        ShipDoc shipDoc = headerToShipDoc(header);
        shipDocMapper.insert(shipDoc);
        if (orderLines.size() != 0) {
            Long i =Long.valueOf("1") ;
            for (OrderLineVO line : orderLines
            ) {
                line.setLineNum(i);
                Long docId = shipDocMapper.selectShipDocId(shipDoc.getOrganizationId(), shipDoc.getDocNumber());
                header.setShipDocId(docId);
                ShipDocLines shipDocLine = orderLineToShipDocLines(header, line);
                shipDocLinesMapper.insert(shipDocLine);
                i++;
            }
        }
    }

    private void deleteOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = shipDocLinesMapper.selectOrderLine(header.getShipDocId());
        ShipDoc shipDoc = headerToShipDoc(header);
        shipDocMapper.delete(shipDoc);
        if (orderLines.size() != 0) {
            for (OrderLineVO line : orderLines
            ) {
                ShipDocLines shipDocLine = orderLineToShipDocLines(header, line);
                shipDocLinesMapper.delete(shipDocLine);
            }
        }
    }

    /**
     * @param shipDoc  新
     * @param shipDoc1 原始
     * @return
     */
    private ShipDoc mergeShipDoc(ShipDoc shipDoc, ShipDoc shipDoc1) {
        if (shipDoc.getShippedTime() != null && !shipDoc.getShippedTime().equals(shipDoc1.getShippedTime())) {
            shipDoc1.setShippedTime(shipDoc.getShippedTime());
        }
        if (shipDoc.getDocType() != null && !shipDoc.getDocType().equals(shipDoc1.getDocType())) {
            shipDoc1.setDocType(shipDoc.getDocType());
        }
        if (shipDoc.getOrganizationId() != null && !shipDoc.getOrganizationId().equals(shipDoc1.getOrganizationId())) {
            shipDoc1.setOrganizationId(shipDoc.getOrganizationId());
        }
        if (shipDoc.getToOrganizationId() != null && !shipDoc.getToOrganizationId().equals(shipDoc1.getToOrganizationId())) {
            shipDoc1.setToOrganizationId(shipDoc.getToOrganizationId());
        }
        if (shipDoc.getDriverName() != null && !shipDoc.getDriverName().equals(shipDoc1.getDriverName())) {
            shipDoc1.setDriverName(shipDoc.getDriverName());
        }
        if (shipDoc.getDriverPhone() != null && !shipDoc.getDriverPhone().equals(shipDoc1.getDriverPhone())) {
            shipDoc1.setDriverPhone(shipDoc.getDriverPhone());
        }
        if (shipDoc.getFreight() != null && !shipDoc.getFreight().equals(shipDoc1.getFreight())) {
            shipDoc1.setFreight(shipDoc.getFreight());
        }
        if (shipDoc.getMemo() != null && !shipDoc.getMemo().equals(shipDoc1.getMemo())) {
            shipDoc1.setMemo(shipDoc.getMemo());
        }

        return shipDoc1;
    }

    /**
     * @param shipDocLines  新
     * @param shipDocLines1 数据库中原数据
     * @return
     */
    private ShipDocLines mergeShipDocLines(ShipDocLines shipDocLines, ShipDocLines shipDocLines1) {
        if (shipDocLines.getInventoryItemId() != null && !shipDocLines.getInventoryItemId().equals(shipDocLines1.getInventoryItemId())) {
            shipDocLines1.setInventoryItemId(shipDocLines.getInventoryItemId());
        }
        if (shipDocLines.getLotNumber() != null && !shipDocLines.getLotNumber().equals(shipDocLines1.getLotNumber())) {
            shipDocLines1.setLotNumber(shipDocLines.getLotNumber());
        }
        if (shipDocLines.getLineQty() != null && !shipDocLines.getLineQty().equals(shipDocLines1.getLineQty())) {
            shipDocLines1.setLineQty(shipDocLines.getLineQty());
        }
        if (shipDocLines.getIssueReqQty() != null && !shipDocLines.getIssueReqQty().equals(shipDocLines1.getIssueReqQty())) {
            shipDocLines1.setIssueReqQty(shipDocLines.getIssueReqQty());
        }

        return shipDocLines1;
    }

    private void updateOrderHeader(OrderHeaderVO header) {
        List<OrderLineVO> orderLines = header.getLines();

        //shipDoc是新修改数据
        ShipDoc shipDoc = headerToShipDoc(header);
        //shipDoc1是原数据
        ShipDoc shipDoc1 = shipDocMapper.selectByPrimaryKey(header.getShipDocId());
        ShipDoc mergeShipDoc1 = mergeShipDoc(shipDoc, shipDoc1);


        shipDocMapper.updateByPrimaryKey(mergeShipDoc1);
        //已经存在行增加或修改的情况下
        if (orderLines.size() != 0) {
            for (OrderLineVO line : orderLines
            ) {
                //转化为行,再查出原来的行,合并,更新
                ShipDocLines newShipDocLine = orderLineToShipDocLines(header, line);
                Long docLineId = shipDocLinesMapper.selectShipDocLineId(newShipDocLine.getOrganizationId(),
                        newShipDocLine.getInventoryItemId(),
                        newShipDocLine.getShipDocId());
                if (null != docLineId) {
                    ShipDocLines shipDocLine = shipDocLinesMapper.selectByPrimaryKey(docLineId);
                    ShipDocLines mergeShipDocLine = mergeShipDocLines(newShipDocLine, shipDocLine);
                    //在同仓库和同物料的情况下判断是不是新建的行,如果是则抛出异常
                        if (shipDocLine.getLineNum().equals(newShipDocLine.getLineNum())) {
                            shipDocLinesMapper.updateByPrimaryKey(mergeShipDocLine);
                        }else {
                            throw new DatasetException("所属仓库与物料已经存在",newShipDocLine.getLineNum());
                        }

                } else {
                    Long lineNum = shipDocLinesMapper.selectShipDocLineNum(newShipDocLine.getShipDocId());
                    if (lineNum != null) {
                        Long i=lineNum+1;
                        newShipDocLine.setLineNum(i);
                        shipDocLinesMapper.insert(newShipDocLine);
                    }else {
                        newShipDocLine.setLineNum(Long.valueOf("1"));
                        shipDocLinesMapper.insert(newShipDocLine);
                    }

                }

            }
        }
    }
}

在行的编辑页面要实现单个删除行:需要自己重写删除方法

@Service
@Transactional(rollbackFor = Exception.class)
@Dataset("OrderLine")
public class OrderLineServiceImpl  implements OrderLineService, IDatasetService<OrderLineVO> {

    @Autowired
    private ShipDocLinesMapper shipDocLinesMapper;

    @Autowired
    private ShipDocMapper shipDocMapper;

    private Long id;

    @Override
    public List<?> queries(Map<String, Object> body, int page, int pageSize, String sortname, boolean isDesc) {
        Object shipDocId =  body.get("shipDocId");
        id =Long.valueOf(shipDocId.toString());
        return shipDocLinesMapper.selectOrderLine(id);
    }

    @Override
    public List<OrderLineVO> mutations(List<OrderLineVO> list) {
        for (OrderLineVO line : list) {
            switch (line.get__status()) {
                case DELETE:
                    deleteOrderLine(line);
                    break;
                default:
                    break;
            }
        }
        return list;
    }

    private void deleteOrderLine(OrderLineVO line) {
        ShipDoc shipDoc = shipDocMapper.selectByPrimaryKey(id);
        Long id1 = shipDocLinesMapper.selectShipDocLineId(shipDoc.getOrganizationId(), line.getInventoryItemId(), id);
        if (id1 != null) {
            shipDocLinesMapper.deleteByPrimaryKey(id1);
        }
    }
}

创建OrderHeader的DataSet.js和OrderLine的DataSet.js

注意要对其中fields和queries fields属性中的lov进行数据绑定(bind),不然在行的页面不知道加载什么数据。注意在这里fields中的两个lov,他们都绑定了organizationId;但是我在头对应的react组件中用来展示的是绑定的数据,就没有用这个lov对象。但是在行的DataSet中,也有一个lov,只是因为这里不光展示数据,还要能通过lov编辑数据,所以就设置与前面的lov的name相同的行name。

function generateCode() {
    let date = new Date();
    let number = Math.round((Math.random()*1000));
    var month = ("0" + (date.getMonth() + 1)).slice(-2);
    while(number.toString().length<4){
        number ="0"+number;
    }
    return "D"+date.getFullYear()+""+month+""+date.getDate()+""+number;
}


export default {
    //主键字段名,一般用作级联行表的查询字段
    primaryKey: 'shipDocId',
    autoQuery: true,
    pageSize: 20,
    //对应后台ds的name,自动生成约定的submitUrl, queryUrl, tlsUrl
    name: 'OrderHeader',
    //与后端对应的列的描述
    fields: [
        {name: 'docNumber', type: 'string', label: '单据号', defaultValue: generateCode()},
        {name: 'docType', type: 'string', label: '单据类型', lookupCode: 'XXFND_DOC_TYPE'},
        {name: 'shippmentStatus', type: 'string', label: '单据状态', lookupCode: 'XXFND_SHIPPMENT_STATUS'},
        {name: 'materialItem1', type: 'object', textField:'organizationId', label: '仓库ID',lovCode: 'XXINV_ISSUE_ORGANIZATION'},
        { name: 'organizationId', type: 'number', label: '仓库ID', bind: 'materialItem1.organizationId' },
        {name: 'materialItem2', type: 'object',textField:'organizationId', label: '目标仓库ID',lovCode: 'XXINV_ISSUE_ORGANIZATION'},
        { name: 'toOrganizationId', type: 'number', label: '仓库ID', bind: 'materialItem2.organizationId' },
        {name: 'driverName', type: 'string', label: '司机姓名'},
        {name: 'driverPhone', type: 'string', label: '司机电话'},
        {name: 'createdBy', type: 'string', label: '创建人number'},
        {name: 'freight', type: 'string', label: '运费'},
        {name: 'memo', type: 'string', label: '备注'},
        {name: 'shippedTime', type: 'datetime', label: '发货时间'},
    ],
    //查询字段,自动生成查询组件
    queryFields: [
        { name: 'shipDoc', type: 'object', textField: 'docNumber', label: '单据号', lovCode: 'XXINV_SHIP_DOC' },
        { name: 'docNumber', type: 'string', label: '单据号', bind: 'shipDoc.docNumber' },
        { name: 'materialItem3', type: 'object',textField:'organizationId', label: '仓库ID', lovCode: 'XXINV_ISSUE_ORGANIZATION' },
        { name: 'organizationId', type: 'number', label: '仓库ID', bind: 'materialItem3.organizationId' },
        { name: 'shippmentStatus', type: 'string', label: '单据状态', lookupCode: 'XXFND_SHIPPMENT_STATUS'},
        { name: 'materialItem4', type: 'object',textField: 'itemNumber', label: '物料编码', lovCode: 'XXINV_ITEMS' },
        { name: 'inventoryItemId', type: 'number', label: 'ERP物料ID', bind: 'materialItem4.inventoryItemId' },
        { name: 'issuedFlag', type: 'string', label: '行发货状态', lookupCode: 'XXFND_SHIPLINE_ISSUED_FLAG' },
        { name: 'docType', type: 'string', label: '单据类型', lookupCode: 'XXFND_DOC_TYPE' },
    ],
};

行的字段显示就比较简单了,不过也是需要数据绑定的:

export default {
    name: 'OrderLine',
    fields: [
        { name: 'lineNum', type: 'string', label: '行号'},
        { name: 'materialItem', type: 'object',textField: 'itemNumber', required: true , label: '物料', lovCode: 'XXINV_ITEMS'},
        { name: 'inventoryItemId', type: 'number', label: 'ERP物料ID', bind: 'materialItem.inventoryItemId' },
        { name: 'itemNumber',type: 'string', label: '物料编码', bind: 'materialItem.itemNumber'},
        { name: 'lotControl', type: 'string', label: '批次控制',},
        { name: 'description', type: 'string', label: '物料描述'},
        { name: 'lotNumber', type: 'string', label: '批次' },
        { name: 'lineQty', type: 'number', label: '行数量'},
        { name: 'issueReqQty', type: 'number', label: '待发货数量'},
        { name: 'shippedQty', type: 'number', label: '已发货数量' },
    ],
};

开发头和行react组件

先开发头的,其中我把编辑和查看作为参数写死(0和1)传入

import React from 'react';
import {Button, IntlField, Modal, Table, Tooltip} from 'choerodon-ui/pro';
import OrderLineModal from './OrderLineModal';

const { Column } = Table;
const modalKey = Modal.key();

export default ({ headerDS, lineDS }) => {
    let isCancel;
    let created;

    /**
     * 编辑修改
     * 数据校验成功时保存
     */
    async function handleOnOkOrderLineModal() {
        isCancel = false;
        if (await headerDS.current.validate()) {
            await headerDS.submit();
        } else {
            return false;
        }
    }

    function handleOnCancelOrderLineModal() {
        isCancel = true;
    }

    /**
     * 关闭编弹窗.
     *
     */
    function handleOnCloseOrderLineModal() {
        if (isCancel) {
            // 新建时取消,移除dataSet记录
            if (created) {
                headerDS.remove(created);
            } else {
                // 修改时取消 重置当前记录数据
                headerDS.current.reset();
                lineDS.reset();
            }
        }
        // 重置新建记录标记
        created = null;
    }

    /**
     * 打开弹窗.
     * @param shipDocId 头Id
     * @param enabled
     */
    function openOrderLineModal(shipDocId,enabled) {
        if (!shipDocId) {
            created = headerDS.create();
        }
        // 如果是编辑状态 单号不可编辑
        const isEditDisabled = !!shipDocId;
        // 如果为启用状态 只可以进行查看 不能编辑数据
        const isEnableDisabled = (isEditDisabled) && (enabled === 1);
        // 如果为启用状态 编码规则行,不可被选中
        lineDS.selection = isEnableDisabled ? false : 'multiple';
        Modal.open({
            //唯一键, 当destroyOnClose为false时,必须指定key。
            // 为了避免与其他modal的key重复,可通过Modal.key()来获取唯一key。
            key: modalKey,
            //标题
            title: shipDocId ? '编辑' : '添加',
            //抽屉模式
            drawer: true,
            //关闭时是否销毁
            destroyOnClose: true,
            //同时显示ok和cancel按钮,false的时候只显示ok按钮
            okCancel: !isEnableDisabled,
            //确认按钮文字
            okText: !isEnableDisabled ? '保存' : '关闭',
            //点击确定回调,返回false Promise.resolve(false)或
            // Promise.reject()不会关闭, 其他自动关闭
            onOk: !isEnableDisabled ? handleOnOkOrderLineModal : handleOnCancelOrderLineModal,
            //点击取消回调,返回false Promise.resolve(false)或
            // Promise.reject()不会关闭, 其他自动关闭
            onCancel: handleOnCancelOrderLineModal,
            //关闭后回调
            afterClose: handleOnCloseOrderLineModal,
            children: (
                <OrderLineModal headerDS={headerDS} lineDS={lineDS} isEditDisabled={isEditDisabled} isEnableDisabled={isEnableDisabled} />
            ),
            style: {
                width: 1100,
            },
        });
    }

    const addBtn = (
        <Button
            icon="playlist_add"
            funcType="flat"
            color="blue"
            onClick={() => openOrderLineModal(null, null)}
        >
            {'添加'}
        </Button>
    );

    const excelBtn = (
        <Button
            icon="format_align_justify"
            funcType="raised"
            color="blue"
        >
            {'Excel导出'}
        </Button>
    );
    /**
     * 渲染表格内容.
     */
    return (
        <Table buttons={[excelBtn,'delete',addBtn]} dataSet={headerDS} queryFieldsLimit={8}>
            <Column name="docNumber"  />
            <Column name="docType"  />
            <Column name="shippmentStatus"  />
            <Column name="organizationId" />
            <Column name="toOrganizationId" />
            <Column name="driverName" />
            <Column name="driverPhone" />
            <Column name="createdBy" />
            <Column header={'编辑'} align="center" width={120}
                    renderer={({ record})=>{
                        return (
                            <Button
                                funcType="flat"
                                icon={'mode_edit'}
                                onClick={() => openOrderLineModal(record.get('shipDocId'),0)}
                             />

                        );
                    }}/>
            <Column
                header={'查看'}
                align="center"
                width={120}
                renderer={({ record}) => {
                    return (
                            <Button
                                funcType="flat"
                                icon={'visibility'}
                                onClick={() => openOrderLineModal(record.get('shipDocId'),1)}
                            />
                    );
                }}
            />
        </Table>
    );

};

再开发行的react组件。行中form标签中的就是头传过来的字段,其中的时间 代码维护 lov的标签需要使用对应的。 注意名称必须对应。时间空间有个import moment from 'moment';

import React from 'react';
import {
    Button,
    CheckBox,
    DatePicker,
    DateTimePicker,
    Form,
    Lov,
    NumberField,
    Select,
    Table,
    TextField
} from 'choerodon-ui/pro';
import moment from 'moment';

const { Column } = Table;

export default ({ headerDS, lineDS, isEditDisabled, isEnableDisabled }) => {


    let btnGroup = [];
    if (!isEnableDisabled) {
        btnGroup = ['add', 'delete'];
    }


    return (
        <div>
            <Form
                columns={3}
                abelWidth={100}

            >
                <TextField name="docNumber" label={'单号'} dataSet={headerDS} required disabled={isEditDisabled}/>
                <DateTimePicker name="shippedTime" label={'发货时间'} dataSet={headerDS}  disabled={isEnableDisabled} min={moment()}/>
                <Select name="docType" label={'单据类型'} dataSet={headerDS} required disabled={isEnableDisabled}/>
                <Lov  name="materialItem1" label={'仓库'} dataSet={headerDS} required disabled={isEnableDisabled} />
                <Lov  name="materialItem2" label={'接收仓库'} dataSet={headerDS}  disabled={isEnableDisabled} />
                <Select name="shippmentStatus" label={'发货单状态'} dataSet={headerDS} required  disabled={isEditDisabled} />
                <TextField name="driverName" label={'司机姓名'} dataSet={headerDS}  disabled={isEnableDisabled} />
                <TextField name="driverPhone" label={'司机电话'} dataSet={headerDS}  disabled={isEnableDisabled} />
                <TextField name="freight" label={'运费'} dataSet={headerDS}  disabled={isEnableDisabled} />
                <TextField name="memo" label={'备注'} dataSet={headerDS}  disabled={isEnableDisabled} />
                <TextField name="createdBy" label={'创建人'} dataSet={headerDS}  disabled={isEditDisabled} />
            </Form>
            <Table
                buttons={btnGroup}
                dataSet={lineDS}
                header={'行'}
            >
                <Column name="lineNum"  label={'行号'} />
                <Column name="materialItem" label={'物料编码'} editor={<Lov />}/>
                <Column name="lotControl"  label={'批次控制'} />
                <Column name="description"  label={'物料描述'} />
                <Column name="lotNumber"  label={'批次'} editor={<NumberField/>}/>
                <Column name="lineQty"  label={'行数量'} editor={<NumberField/>}/>
                <Column name="issueReqQty"  label={'待发货数量'} editor={<NumberField/>}/>
                <Column name="shippedQty"  label={'已发货数量'} editor={<NumberField/>}/>
            </Table>
            <div>
                <Button
                    icon="format_align_justify"
                    funcType="raised"
                    color="blue"
                >
                    {'打印'}
                </Button>
                <Button
                    icon="format_align_justify"
                    funcType="raised"
                    color="blue"
                >
                    {'单据确认'}
                </Button>
                <Button
                    icon="format_align_justify"
                    funcType="raised"
                    color="blue"
                >
                    {'确认发货'}
                </Button>
            </div>
        </div>
    );
};

编辑xlsx,添加路由

这里的头与行的顺序最好不要打乱,我之前乱了一行,初始化表,插入不了/ship-order/order下面的数据

xlsx添加服务.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容

  • 【日精进第33天】 一、【学~勤学】 ①日常课诵 诵读《道德经》第7章和注解一遍,《京瓷哲学》第七条,《定位》第四...
    julia2000阅读 235评论 0 1
  • 文|素言锦心 昨天周日送孩子上课,距离下课还有两小时,回家太耽误时间,还没忙完手头的事情就要动身再去接孩子,于是很...
    素言锦心阅读 1,114评论 3 2
  • 今天学习了bootstrap,预计再学习一天
    1eb034fb5715阅读 77评论 0 0
  • 早恋,从这个词语诞生的那一天,就注定它是个贬义词。因为它是上一辈对下一辈青春期青涩的初恋的鄙视。 早恋,字面意思过...
    刘苔米阅读 275评论 0 1