API参考 - 筋斗云前端(桌面Web版)

最后更新:2023-12-13

@module WUI

1 对象管理功能

1.1 定义列表页和详情页

1.2 添加入口按钮

1.3 定义页面初始化函数

1.4 定义对话框的初始化函数

1.5 列表页中的常见需求

1.5.1 列表页中的列,以特定格式展现

1.5.2 排序与分页

1.5.3 列表导出Excel文件

1.5.4 datagrid增强项

1.5.5 treegrid集成

1.6 详情页对话框的常见需求

1.6.1 通用查询

1.6.2 设计模式:关联选择框

1.6.3 picId字段显示图片

1.6.4 List字段显示为多个选项框

1.7 设计模式:展示关联对象

2 对话框功能

2.1 定义对话框

2.2 显示对话框

2.3 数据存取型对话框

2.4 页面传参数给对话框

2.5 示例:页面与对话框复用 (v5.1)

2.6 只读对话框

2.7 只读字段:使用disabled和readonly属性

3 模块化开发

3.1 批量更新、批量删除

3.1 页面模板支持

3.2 按需加载依赖库

4 参考文档说明

筋斗云前端框架-Web应用桌面版

此框架实现与筋斗云服务端接口的无缝整合。在界面上以jquery-easyui库为基础展示列表、Tab页等。
参考应用 web/store.html - 商户管理端应用。

1 对象管理功能

设计模式:列表页与详情页。

以订单对象Order为例:为订单对象增加“列表页”和“详情页”。

列表页应包含分页功能,默认只显示“未完成”订单。
点击列表中一项(一个订单),可显示详情页,即订单详情,并可进行查找、更新等功能。

1.1 定义列表页和详情页

@key #my-pages 包含所有页面、对话框定义的容器。

@key my-obj DOM属性,标识服务端对象

@key my-initfn DOM属性,标识页面或对话框的初始化函数,首次显示页面/对话框时调用。

列表页使用逻辑页面定义如下(放在div#my-pages之下),它最终展示为一个tab页:

<div id="my-pages" style="display:none">
    ...
    <script type="text/html" id="tpl_pageOrder">
        <div class="pageOrder" title="订单管理" my-initfn="initPageOrder">
            <table id="tblOrder" style="width:auto;height:auto">
                <thead><tr>
                    <th data-options="field:'id', sortable:true, sorter:intSort">订单号</th>
                    <th data-options="field:'userPhone', sortable:true">用户联系方式</th>
                    <th data-options="field:'createTm', sortable:true">创建时间</th>
                    <th data-options="field:'status', jdEnumMap: OrderStatusMap, formatter:Formatter.enum(OrderStatusMap), styler:Formatter.enumStyler({PA:'Warning'}), sortable:true">状态</th>
                    <th data-options="field:'dscr', sortable:true">描述</th>
                    <th data-options="field:'cmt'">用户备注</th>
                </tr></thead>
            </table>
        </div>
    </script>
</div>

注意:

详情页展示为一个对话框,也将它也放在 div#my-pages 下。定义如下(此处为展示原理已简化):

<script type="text/html" id="tpl_dlgOrder">
    <div id="dlgOrder" my-obj="Ordr" my-initfn="initDlgOrder" title="用户订单" style="width:520px;height:500px;">  
        <form method="POST">
            订单号:<input name="id" disabled></td>
            订单状态:
                    <select name="status">
                        <option value="">&nbsp;</option>
                        <option value="CR">未付款</option>
                        <option value="PA">待服务(已付款)</option>
                        <option value="ST">正在服务</option>
                        <option value="RE">已服务(待评价)</option>
                        <option value="RA">已评价</option>
                        <option value="CA">已取消</option>
                    </select>
        用户备注:<textarea name="cmt" rows=3 cols=30></textarea>
        </form>
    <div>
</script>

注意:

@see showObjDlg

@see showDlg

以上定义了订单对象的列表页和详情页,围绕对象"Order", 按规范,我们定义了以下名字:

1.2 添加入口按钮
<a href="#pageOrder" class="easyui-linkbutton" icon="icon-ok">订单管理</a><br/><br/>
1.3 定义页面初始化函数

打开页面后,页面的生存周期如下:

@key event-pagecreate,pageshow,pagedestroy 页面事件

@key wui-pageName 属性:页面名

@key .wui-page 页面类

订单列表页的初始化,需要将列表页(代码中jpage)、列表(代码中jtbl)与详情页(代码中jdlg)关联起来,实现对话增删改查各项功能。

function initPageOrder() 
{
    var jpage = $(this);
    var jtbl = jpage.find("#tblOrder");
    var jdlg = $("#dlgOrder");

    // 注意:此处定义显示哪些缺省操作按钮:
    // r-refresh/刷新, f-find/查找, s-set/更新。参考 WUI.dg_toolbar.
    // 如果不定义则所有操作按钮都展示。
    jtbl.jdata().toolbar = "rfs";

    // 当天订单
    var query1 = {cond: "createTm between '" + new Date().format("D") + "' and '" + new Date().addDay(1).format("D") + "'"};
    // var query1 = WUI.getQueryParam({createTm: new Date().format("D") + "~" + new Date().addDay(1).format("D")});
    // 显示待服务/正在服务订单
    var query2 = {cond: "status='CR' OR status='PA' OR status='ST'"};
    // var query2 = WUI.getQueryParam({status: "CR,PA,ST"});

    function getTodoOrders()
    {
        WUI.reload(jtbl, null, query2);
    }
    function getTodayOrders()
    {
        WUI.reload(jtbl, null, query1);
    }
    var btn1 = {text: "今天订单", iconCls:'icon-search', handler: getTodayOrders};
    var btn2 = {text: "所有未完成", iconCls:'icon-search', handler: getTodoOrders};

    var dgOpt = {
        // 设置查询接口
        url: WUI.makeUrl(["Ordr", "query"], {res:"*,createTm,userPhone"}),
        // 设置缺省查询条件
        queryParams: query1,
        // 设置工具栏上的按钮,默认有增删改查按钮,"export"表示"导出到Excel"的按钮,btn1, btn2是自定义按钮,"-"表示按钮分隔符。
        toolbar: WUI.dg_toolbar(jtbl, jdlg, "export", "-", btn1, btn2),
        // 双击一行,应展示详情页对话框
        onDblClickRow: WUI.dg_dblclick(jtbl, jdlg)
    };
    jtbl.datagrid(dgOpt);
}

@see showPage

@see dg_toolbar

@see dg_dblclick

@see makeUrl

1.4 定义对话框的初始化函数

@key example-dialog-event

默认对话框中由于设定了底层对象(my-obj)及属性关联(form中带name属性的组件,已关联对象属性),因而可自动显示和提交数据。

特别地,某些属性不宜直接展示,例如属性“人物头像”,服务器存储的是图片id(picId),而展示时应显示为图片而不是一个数字;
或者如“权限列表”属性,服务器存储的是逗号分隔的一组权限比如"emp,mgr",而展示时需要为每项显示一个勾选框。
这类需求就需要编码控制。

相关事件:

@see beforeshow show 对话框中form显示前后

对话框类名:

@see .wui-dialog

function initDlgOrder()
{
    var jdlg = $(this);
    jdlg.on("beforeshow", onBeforeShow)
        .on("show", onShow)
        .on("validate", onValidate)
        .on("retdata", onRetData);

    function onBeforeShow(ev, formMode, opt) {
        // beforeshow用于设置字段是否隐藏、是否可编辑;或是设置opt(即WUI.showDlg的opt)。

        var objParam = opt.objParam;
        var forAdd = formMode == FormMode.forAdd;
        var forSet = formMode == FormMode.forSet;

        jdlg.find(".notForFind").toggle(formMode != FormMode.forFind);

        // WUI.toggleFields也常用于控制jfrm上字段显示或jtbl上列显示
        var type = opt.objParam && opt.objParam.type;
        var isMgr = g_data.hasRole("mgr"); // 最高管理员
        var isAdm = g_data.hasRole("mgr,emp"); // 管理员
        WUI.toggleFields(jfrm, {
            type: !type,
            status: !type || type!="公告",
            atts: isAdm
        });

        // 根据权限控制字段是否可编辑。注意:find模式下一般不禁用。
        if (formMode != FormMode.forFind) {
            $(frm.empId).prop("disabled", !isMgr);
            $(frm.status).prop("disabled", forAdd || !isAdm);
            $(frm.code).prop("disabled", !isAdm);
        }
    }
    function onShow(ev, formMode, initData) {
        // 常用于add模式下设置初值,或是set模式下将原值转换并显示。
        // initData是列表页中一行对应的数据,框架自动根据此数据将对应属性填上值。
        // 如果界面上展示的字段无法与属性直接对应,可以在该事件回调中设置。
        // hiddenToCheckbox(jdlg.find("#divPerms"));
        if (forAdd) {
            $(frm.status).val("CR");
        }
        else if (forSet) {
            // 显示成表格
            jdlg.find("#tbl1").datagrid(...);
        }
    }
    function onValidate(ev, formMode, initData, newData) {
        // 在form提交时,所有带name属性且不带disabled属性的对象值会被发往服务端。
        // 此事件回调可以设置一些界面上无法与属性直接对应的内容。
        // 额外要提交的数据可放在隐藏的input组件中,或(v5.1)这里直接设置到newData对象中。
        // checkboxToHidden(jdlg.find("#divPerms"));
    }
    function onRetData(ev, data, formMode) {
        var formMode = jdlg.jdata().mode;
        if (formMode == FormMode.forAdd) {
            alert('返回ID: ' + data);
        }
    }
}

在onBeforeShow中一般设置字段是否显示(show/hide/toggle)或只读(disabled),以及在forAdd/forFind模式时为opt.data设置初始值(forSet模式下opt.data已填上业务数据);
之后框架用opt.data数据填充相应字段,如需要补填或修改些字段(比如显示图片),可在onShow中处理,也可以直接在onBeforeShow中用setTimeout来指定,如:

function onBeforeShow(ev, formMode, opt) {
    // ... 根据formMode等参数控制某些字段显示隐藏、启用禁用等...
    var frm = jdlg.find("form")[0];
    var isFind = formMode == FormMode.forFind;
    frm.type.disabled = !isFind;
    // 这里可以对opt.data赋值,但不要直接为组件设置值,因为接下来组件值会被opt.data中的值覆盖。

    setTimeout(onShow);
    function onShow() {
        // 这里可根据opt.data直接为input等组件设置值。便于使用onBeforeShow中的变量
    }
}

@see checkboxToHidden (有示例)

@see hiddenToCheckbox

@see imgToHidden

@see hiddenToImg (有示例)

1.5 列表页中的常见需求

框架中,对象列表通过easyui-datagrid来展现。
注意:由于历史原因,我们没有使用datagrid中的编辑功能。

参考:http://www.jeasyui.net/plugins/183.html
教程:http://www.jeasyui.net/tutorial/148.html

1.5.1 列表页中的列,以特定格式展现

@key datagrid.formatter

@key datagrid.styler

示例一:显示名称及颜色

订单状态字段定义为:

- status: 订单状态. Enum(CR-新创建,RE-已服务,CA-已取消). 

在显示时,要求显示其中文名称,且根据状态不同,显示不同的背景颜色。

在table中设置formatter与styler选项:

<div class="pageOrder" title="订单管理" my-initfn="initPageOrder">
    <table id="tblOrder" style="width:auto;height:auto" title="订单列表">
        <thead><tr>
            <th data-options="field:'id', sortable:true, sorter:intSort">订单号</th>
            ...
            <th data-options="field:'status', jdEnumMap: OrderStatusMap, formatter:Formatter.enum(OrderStatusMap), styler:Formatter.enumStyler({PA:'Warning', RE:'Disabled', CR:'#00ff00', null: 'Error'}), sortable:true">状态</th>
        </tr></thead>
    </table>
</div>

formatter用于控制Cell中的HTML标签,styler用于控制Cell自己的CSS style, 常用于标记颜色.
在JS中定义:

var OrderStatusMap = {
    CR: "未付款", 
    RE: "已服务", 
    CA: "已取消"
};
Formatter = $.extend(WUI.formatter, Formatter);

上面Formatter.enum及Formatter.enumStyler是框架预定义的常用项,也可自定义formatter或styler,例:

var OrderColumns = {
    status: function (value, row) {
        if (! value)
            return;
        return OrderStatusMap[value] || value;
    },
    statusStyler: function (value, row) {
        var colors = {
            CR: "#000",
            RE: "#0f0",
            CA: "#ccc"
        };
        var color = colors[value];
        if (color)
            return "background-color: " + color;
    },
    ...
}

注意:

@see formatter 通用格式化函数

一些其它示例:

var Formatter = {
    // 显示数值
    number: function (value, row) {
        return parseFloat(value);
    },
    // 订单编号,显示为一个链接,点击就打开订单对话框该订单。
    orderId: function (value, row) {
        if (value) {
            return WUI.makeLinkTo("#dlgOrder", row.orderId, value);
        }
    },
    // 显示状态的同时,设置另一个本地字段,这种字段一般以"_"结尾,表示不是服务器传来的字段,例如
    // <th data-options="field:'hint_'">提醒事项</th>
    status: function (value, row) {
        if (value) {
            if (value == "PA") {
                row.hint_ = "请于2小时内联系";
            }
            return StatusMap[value] || value;
        }
    }
};

@see makeLinkTo 生成对象链接,以便点击时打开该对象的详情对话框。

1.5.2 排序与分页

@key datagrid.sortable

@key datagrid.sorter

使用sortable:true指定该列可排序(可点击列头排序),用sorter指定排序算法(缺省是字符串排序),例如:

<th data-options="field:'name', sortable:true">姓名</th>
<th data-options="field:'id', sortable:true, sorter:intSort">编号</th>
<th data-options="field:'score', sortable:true, sorter:numberSort">评分</th>

框架提供了intSort,numberSort这些函数用于整数排序或小数排序。也可以自定义函数。示例:

function intSort(a, b)
{
    return parseInt(a) - parseInt(b);
}

注意:

@see intSort numberSort

如果打开数据表就希望按某一列排序,可设置:

jtbl.datagrid({
    ...
    sortName: 'id',
    sortOrder: 'desc'
});

手工点击列标题栏排序,会自动修改这两个属性。
在添加数据时,如果当前sortOrder是倒序,则新数据显示在表格当前页的最前面,否则显示在最后。

框架对datagrid还做了以下缺省设置:

如果需要禁用分页,可以设置:

jtbl.datagrid({
    url: WUI.makeUrl("Ordr.query", {"pagesz": -1}), // -1表示取后端允许的最大数量
    pagination: false, // 禁用分页组件
    ...
});
1.5.3 列表导出Excel文件

(支持版本5.0)

除了默认地增删改查,还可为数据表添加标准的“导出Excel”操作,可自动按表格当前的显示字段、搜索条件、排序条件等,导出表格。
只需在dg_toolbar函数的参数中加上"export"(表示导出按钮),如:

jtbl.datagrid({
    url: WUI.makeUrl("User.query"),
    toolbar: WUI.dg_toolbar(jtbl, jdlg, "export"),
    onDblClickRow: WUI.dg_dblclick(jtbl, jdlg)
});

导出字段由jtbl对应的表格的表头定义,如下面表格定义:

<table id="tblOrder" style="width:auto;height:auto" title="订单列表">
    ...
    <th data-options="field:'id'">编号</th>
    <th data-options="field:'status'">状态</th>
    <th data-options="field:'hint_'">友情提示</th>
</table>

它生成的res参数为"id 编号, status 状态"。"hint_"字段以下划线结尾,它会被当作是本地虚拟字段,不会被导出。

table上的title属性可用于控制列表导出时的默认文件名,如本例导出文件名为"订单列表.xls"。
如果想导出表中没有显示的列,可以设置该列为隐藏,如:

    <th data-options="field:'userId', hidden:true">用户编号</th>

@key jdEnumMap datagrid中th选项, 在导出文件时,枚举变量可显示描述

对于枚举字段,可在th的data-options用formatter:WUI.formatter.enum(map)来显示描述,在导出Excel时,需要设置jdEnumMap:map属性来显示描述,如

    <th data-options="field:'status', jdEnumMap: OrderStatusMap, formatter: WUI.formatter.enum(OrderStatusMap)">状态</th>

OrderStatusMap在代码中定义如下

var OrderStatusMap = {
    CR: "未付款", 
    PA: "待服务"
}

它生成的res参数为"id 编号, status 状态=CR:未付款;PA:待服务"。筋斗云后端支持这种res定义方式将枚举值显示为描述。

@see dg_toolbar 指定列表上的操作按钮

@see getExportHandler 自定义导出Excel功能

@see getQueryParamFromTable 根据当前datagrid状态取query接口参数

HINT: 点“导出”时会直接下载文件,看不到请求和调用过程,如果需要调试导出功能,可在控制台中设置 window.open=$.get 即可在chrome中查看请求响应过程。

1.5.4 datagrid增强项

easyui-datagrid已适配筋斗云协议调用,底层将发起callSvr调用请求(参考dgLoader)。
此外,增加支持url_属性,以便初始化时不发起调用,直到调用"load"/"reload"方法时才发起调用:

jtbl.datagrid({
    url_: WUI.makeUrl("Item.query", {res:"id,name"}), // 如果用url则会立即用callSvr发起请求。
    ...
});
// ...
jtbl.datagrid("load", {cond: "itemId=" + itemId});
jtbl.datagrid("reload");

如果接口返回格式不符合,则可以使用loadData方法:

// 接口 Item.get() -> {item1=[{srcItemId, qty}]}
callSvr("Item.get", {res:"item1"}, function (data) {
    jtbl.datagrid("loadData", data.item1); // 是一个对象数组
});

datagrid默认加载数据要求格式为{total, rows},框架已对返回数据格式进行了默认处理,兼容筋斗云协议格式(参考dgLoadFilter)。

var rows = [ {id:1, name:"name1"}, {id:2, name:"name2"} ];
jtbl.datagrid("loadData", {tota:2, rows: rows});
// 还支持以下三种格式
jtbl.datagrid("loadData", rows);
jtbl.datagrid("loadData", {h: ["id","name"], d: [ [1, "name1"], [2, "name2"]}); // 筋斗云query接口默认返回格式。
jtbl.datagrid("loadData", {list: rows}); // 筋斗云query接口指定fmt=list参数时,返回这种格式
1.5.5 treegrid集成

@key treegrid

后端数据模型若要支持树类型,须在表中有父节点字段(默认为fatherId), 即可适配treegrid. 典型的表设计如下:

@Dept: id, code, name, fatherId, level

在初始化页面时, 与datagrid类似: pageItemType.js

var dgOpt = {
    // treegrid查询时不分页. 设置pagesz=-1. (注意后端默认返回1000条, 可设置放宽到10000条. 再多应考虑按层级展开)
    url: WUI.makeUrl("ItemType.query", {pagesz: -1}),
    toolbar: WUI.dg_toolbar(jtbl, jdlg),
    onDblClickRow: WUI.dg_dblclick(jtbl, jdlg)
    // treeField: "code"  // 树表专用,表示在哪个字段上显示折叠,默认为"id"
    // fatherField: "id" // 树表专用,WUI扩展字段,表示父节点字段,默认为"fatherId"
};
// 用treegrid替代常规的datagrid
jtbl.treegrid(dgOpt);

如果数据量非常大, 可以只显示第一层级, 展开时再查询.
仅需增加初始查询条件(只查第一级)以及一个判断是否终端结点的回调函数isLeaf (否则都当作终端结点将无法展开):

var dgOpt = {
    queryParams: {cond: "fatherId is null"},
    isLeaf: function (row) {
        return row.level>1;
    },
    ...
};
jtbl.treegrid(dgOpt);

[通过非id字段关联父节点的情况]

比如通过fatherCode字段关联到父节点的code字段:

@Dept: id, code, fatherCode

则可以指定idField, 这样调用:

var dgOpt = {
    idField: "code",
    fatherField: "fatherCode",
    ...
};
jtbl.treegrid(dgOpt);
1.6 详情页对话框的常见需求
1.6.1 通用查询

在对话框中按快捷键"Ctrl-F"可进入查询模式。
详情页提供通用查询,如:

手机号: <input name="phone">  
注册时间: <input name="createTm">

可在手机号中输入"137*",在注册时间中输入">=2017-1-1 and <2018-1-1" (或用 "2017-1-1~2018-1-1"),这样生成的查询参数为:

{ cond: "phone like '137%' and (createTm>='2017-1-1' and createTm<'2018-1-1')" }

@see getQueryCond 查询条件支持

@see getQueryParam 生成查询条件

@key .wui-find-field 用于查找的字段样式

可设置该样式来标识哪些字段可以查找。一般设置为黄色。

@key .notForFind 指定非查询条件

不参与查询的字段,可以用notForFind类标识,如:

登录密码: <input class="notForFind" type="password" name="pwd">

@key .wui-notCond 指定独立查询条件

如果查询时不想将条件放在cond参数中,可以设置wui-notCond类标识,如:

状态: <select name="status" class="my-combobox wui-notCond" data-options="jdEnumList:'0:可用;1:禁用'"></select>

如果不加wui-notCond类,生成的查询参数为:{cond: "status=0"};加上后,生成查询参数如:{status: 0}.

(v5.3)

@key wui-find-hint 控制查询条件的生成。(v5.5)

示例:

视频代码 <input name="code" wui-find-hint="s">

当输入'126231-191024'时不会当作查询126231到191024的区间。

1.6.2 设计模式:关联选择框

示例:下拉框中显示员工列表 (Choose-from-list / 关联选择框)

@see jQuery.fn.mycombobox

1.6.3 picId字段显示图片

@see hiddenToImg (有示例)

@see imgToHidden

1.6.4 List字段显示为多个选项框

@see hiddenToCheckbox

@see checkboxToHidden  (有示例)

1.7 设计模式:展示关联对象

例如设计有商品表Item, 每个商品属于特定的商户:

@Item: id, storeId, name
- storeId: Integer. 商品所属商户编号。

也就是说,一个商户对应多个商品。要展现商品,可将它放在商户层次之下。
可以这样设计用户操作:在商户列表上增加一个按钮“查看商品”,点击后打开一个新的列表页,显示该商户的商品列表。

定义两个列表页:

<div class="pageStore" title="商户列表" my-initfn="initPageStore">
</div>

<div class="pageItem" title="商户商品" my-initfn="initPageItem">
</div>

为这两个列表页定义初始化函数:

// 商户列表页
function initPageStore()
{
    ...
    var btn1 = {text: "查看商品", iconCls: "icon-search", handler: showItemPage};
    jtbl.datagrid({
        ...
        toolbar: WUI.dg_toolbar(jtbl, jdlg, btn1),
    });

    function showItemPage() {
        var row = WUI.getRow(jtbl);
        if (row == null)
            return;
        // !!! 调用showPage显示新页 !!!
        var pageFilter = {cond: {storeId: row.id}};
        WUI.showPage("pageItem", {title: "商户商品-" + row.name, pageFilter: pageFilter});
        // 要使每个商户都打开一个商品页面而不是共享一个页面,必须保证第二个参数(页面标题)根据商户不同而不一样。
    }
}

注意:

WUI.showPage的第二参数既可以是简单的页面标题(title),也可以是复杂的showPage选项(showPageOpt),下面两个调用是相同的:

WUI.showPage("pageItem", "商户商品-" + row.name, ...);
WUI.showPage("pageItem", {title: "商户商品-" + row.name}, ...);

显然,title随着商户名称不同而不同,这保证了不同商户打开的商品页面不会共用。

而showPageOpt.pageFilter参数会自动加到数据表调用后端的query接口参数中,对列表页进行过滤。

@see showPage

@key .wui-fixedField 固定值字段

(v6) 当打开商户关联的商品列表页时,点开商品明细对话框,商户这个字段(storeId)固定为pageFilter.cond中storeId的值,不可修改。这称为固定值字段(fixedField)。

也可以通过WUI.showDlg/WUI.showObjDlg的opt.fixedFields参数来指定固定值字段(值为field=>value映射),或是通过jdlg设置对话框参数如jdlg.objParam.fixedFields={storeId: row.id}

v5.3引入了wui-fixedField类设置在字段上,v6起已不建议使用。以下仅用于兼容:
当打开对话框时, 标识为.wui-fixedField类的字段会自动从传入的opt.objParam中取值, 如果取到值则将自己设置为只读.

<select name="storeId" class="my-combobox wui-fixedField" data-options="ListOptions.Store()"></select>

2 对话框功能

@key example-dialog

以群发短信功能为例。

假定服务端已有以下接口:

sendSms(phone, content)
phone:: 手机号
content:: 发送内容
2.1 定义对话框

注意:每个带name属性的组件对应接口中的参数。

<div id="dlgSendSms" title="群发短信" style="width:500px;height:300px;">  
    <form method="POST">
        手机号:<input name="phone" class="easyui-validatebox" required>
        发送内容: <textarea rows=5 cols=30 name="content" class="easyui-validatebox" required></textarea>
    </form>
</div>

在form中带name属性的字段上,可以设置class="easyui-validatebox"对输入进行验证。

2.2 显示对话框

可以调用WUI.showDlg,写一个显示对话框的函数:

function showDlgSendSms()
{
    WUI.showDlg("#dlgSendSms", {
        url: WUI.makeUrl("sendSms"),
        onOk: function (data) {  // (v6.0) 可以直接用字符串 'close'
            var jdlg = $(this);
            WUI.closeDlg(jdlg);
            app_show('操作成功!');
        }
    });
}

在showDlg的选项url中指定了接口为"sendSms"。操作成功后,显示一个消息。

(v5.5) 新的编程惯例,建议使用定义对话框接口的方式,写在主应用(如store.js)的接口区域,如:

// 把showDlgSendSms换成DlgSendSms.show
var DlgSendSms = {
    show: function () {
        // 同showDlgSendSms
    }
};

@see showDlg

@see app_show

点击菜单项显示对话框:

<a href="javascript:;" onclick="DlgSendSms.show()" class="easyui-linkbutton" icon="icon-ok">群发短信</a><br/><br/>

可以通过my-initfn属性为对话框指定初始化函数。复杂对话框的逻辑一般都写在初始化函数中。习惯上命令名initDlgXXX,如:

<div id="dlgSendSms" title="群发短信" style="width:500px;height:300px;" my-initfn="initDlgSendSms">

function initDlgSendSms() {
    var jdlg = $(this);
    // 处理对话框事件
    jdlg.on("beforeshow", onBeforeShow)
        .on("validate", onValidate);
    // 处理内部组件事件
    jdlg.find("#btn1").click(btn1_click);
    ...
}
2.3 数据存取型对话框

示例:显示某订单,可设置属性并保存。

假如已做好订单html模板 dlgOrder.html, 调用它:

var orderId = 99;
callSvr("Ordr.get", {id: orderId}, function (data) {
    WUI.showDlg("#dlgOrder", {
        modal: false, // 非模态框
        data: data, // 初始化数据
        forSet: true, // 与data选项合用,如果无数据修改则不提交
        dialogOpt: {maximized: true}, // 打开时最大化
        // reload: true, // 每次都重新加载(测试用)
        url: WUI.makeUrl("Ordr.set", {id: orderId}), // 提交的URL,form中的内容通过POST请求发到后端。由于设置了opt.forSet=true, 只会提交修改部分。
        onOk: 'close'
    })
});

由于是典型的对象操作,一般用前述章节的对象对话框来做。上例相当于:

var orderId = 99;
WUI.showObjDlg("#dlgOrder", FormMode.forSet, {id: orderId});
2.4 页面传参数给对话框

(v5.1)
可以通过showObjDlg(jdlg, mode, opt)中的opt参数,或jdlg.objParam来给对话框传参。
在对话框的beforeshow事件处理中,可通过opt.objParam拿到参数,如:

function initPageBizPartner() {
    var jdlg = $("#dlgSupplier");
    // 设置objParam参数供对话框使用。
    jdlg.objParam = {type: "C", obj: "Customer", title: "客户"}; // opt.title参数可直接设置对话框的标题。参考showObjDlg.
    jtbl.datagrid(toolbar: dg_toolbar(jtbl, jdlg, ...));
    // 点表格上的菜单或双击行时会调用 WUI.showObjDlg
}

function initDlgBizPartner() {
    // ...
    jdlg.on("beforeshow", onBeforeShow);

    function onBeforeShow(ev, formMode, opt) {
        // opt.objParam 中包含前面定义的type, obj, 以及id, mode等参数。
    }
}
2.5 示例:页面与对话框复用 (v5.1)

设计有客户(Customer)和供应商(Supplier)两个虚拟的逻辑对象,它们物理底层都是业务伙伴对象(BizPartner)。
现在只设计一个页面pageBizPartner和一个对话框dlgBizPartner。

菜单中两项:
默认pageBizPartner是供应商,如果要显示为"客户"页,需要明确调用showPage。

<a href="#pageBizPartner">供应商</a>
<a href="javascript:;" onclick="WUI.showPage('pageBizPartner', '客户', ['C']);">客户</a>

在initPageBizPartner函数中,为对话框传递参数objParam:

type = type || "S";
var obj = "type=="S"? "Supplier": "Customer";
jdlg.objParam = {type: type, obj: obj};
// ...

在对话框的beforeshow事件处理中,根据opt.objParam.type确定标题栏:

jdlg.on("beforeshow", function (ev, formMode, opt) {
    opt.title = opt.objParam.type == "C"? "客户": "供应商";
});
2.6 只读对话框

(v5.1)

@key .wui-readonly 只读对话框类名

设置是否为只读对话框只要加上该类:

jdlg.addClass("wui-readonly");
jdlg.removeClass("wui-readonly");
jdlg.toggleClass("wui-readonly", isReadonly);

只读对话框不可输入(在style.css中设定pointer-events为none),点击确定按钮后直接关闭。

注意:在dialog beforeshow事件中,不应直接设置wui-readonly类,因为框架之后会自动设置,导致前面设置无效。正确做法是设置opt.objParam.readonly=true,示例:

jdlg.on("beforeshow", onBeforeShow);
function onBeforeShow(ev, formMode, opt)
{
    var objParam = opt.objParam;
    var ro = (formMode == FormMode.forSet && !!opt.data.usedFlag);
    // beforeshow中设置对话框只读
    objParam.readonly = ro;
}
2.7 只读字段:使用disabled和readonly属性

3 模块化开发

@key wui-script

@key options.pageFolder

允许将逻辑页、对话框的html片段和js片段放在单独的文件中。以前面章节示例中订单对象的列表页(是一个逻辑页)与详情页(是一个对话框)为例:

先在文件page/pageOrder.html中定义逻辑页

<div title="订单管理" wui-script="pageOrder.js" my-initfn="initPageOrder">
    <table id="tblOrder" style="width:auto;height:auto">
        ...
    </table>
</div>

注意:

在html文件的div中可以添加style样式标签:

<div>
    <style>
    table {
        background-color: #ddd;
    }
    </style>
    <table>...</table>
</div>

注意:其中定义的样式(比如这里的table)只应用于当前页面或对话框,因为框架会在加载它时自动限定样式作用范围。

在文件page/pageOrder.js中定义逻辑:

function initPageOrder() 
{
    var jpage = $(this);
    ...
}

这时,就可以用 WUI.showPage("#pageOrder")来显示逻辑页了。

注意:逻辑页的title字段不能和其它页中title重复,否则这两页无法同时显示,因为显示tab页时是按照title来标识逻辑页的。

动态加载页面时,先加载逻辑页html和js文件,再将逻辑页插入应用程序并做系统初始化(如增强mui组件或easyui组件等),然后调用页面的用户初始化函数。
若希望在系统初始化之前做一些操作,应放在用户初始化函数之外。
例如,初始化过程中的服务调用使用批处理:

functio initPageOrder() 
{
    ...
}
WUI.useBatchCall();

在文件page/dlgOrder.html中定义对话框UI:

<div wui-script="dlgOrder.js" my-obj="Ordr" my-initfn="initDlgOrder" title="用户订单" style="width:520px;height:500px;">  
    <form method="POST">
        ...
    </form>
<div>

注意:

在文件page/dlgOrder.js中定义js逻辑:

function initDlgOrder()
{
    var jdlg = $(this);
    ...
}

这时,就可以用 WUI.showObjDlg("#dlgOrder")来显示逻辑页了。

3.1 批量更新、批量删除

(v5.2)
列表页支持两种批量操作模式。

服务端应支持{obj}.setIf(cond){obj}.delIf(cond)接口。

3.1 页面模板支持

定义一个逻辑页面,可以在#my-pages下直接定义,也可以在单独的文件中定义,还可以在一个模板中定义,如:

<script type="text/html" id="tpl_pageOrder">
<div class="pageOrder" title="订单管理" my-initfn="initPageOrder">
...
</div>
</script>

模板用script标签定义,其id属性必须命名为tpl_{逻辑页面名}
这样就定义了逻辑页pageOrder,且在用到时才加载。与从外部文件加载类似,可以不设置class="pageOrder",框架会自动处理。

定义对话框也类似:

<script type="text/html" id="tpl_dlgOrder">
<div id="dlgOrder" my-obj="Ordr" my-initfn="initDlgOrder" title="用户订单" style="width:520px;height:500px;">  
...
</div>
</script>

定义了对话框dlgOrder,这个id属性也可以不设置。
模板用script标签定义,其id属性必须命名为tpl_{对话框名}

注意:

如果将script标签制作的页面模板内嵌在主页中,可能会造成加载时闪烁。
在chrome中,在easyui-layout之后定义任意script标签(哪怕是空内容),会导致加载首页时闪烁,标题栏是黑色尤其明显。
测试发现,将这些个script模板放在head标签中不会闪烁。

这个特性可用于未来WEB应用编译打包。

3.2 按需加载依赖库

@key wui-deferred

(v5.5)

如果页面或对话框依赖一个或一组库,且这些库不想在主页面中用script默认加载,这时可以使用wui-deferred属性。
页面或对话框初始化函数wui-initfn将在该deferred对象操作成功后执行。

示例:想在工艺对话框上使用mermaid库显示流程图,该库比较大,只在这一处使用,故不想在应用入口加载。
可在app.js中添加库的加载函数:

var m_dfdMermaid;
function loadMermaidLib()
{
    if (m_dfdMermaid == null)
        m_dfdMermaid = WUI.loadScript("lib/mermaid.min.js");
    return m_dfdMermaid;
}

在对话框html中用wui-deferred引入依赖库:

<form my-obj="Flow" title="工艺" ... wui-deferred="loadMermaidLib()">

在对话框模块(初始化函数)中就可以直接使用这个库了:

function initDlgFlow()
{
    ...
    mermaid.render("graph", def, function (svg) {
        jdlg.find(".graph").html(svg);
    });
}

4 参考文档说明

以下参考文档介绍WUI模块提供的方法/函数(fn)、属性/变量(var)等,示例如下:

@fn showPage(pageName, title?, paramArr?)  一个函数。参数说明中问号表示参数可缺省。
@var options 一个属性。
@class batchCall(opt?={useTrans?=0}) 一个JS类。
@key example-dialog key表示一般关键字。前缀为"example-"用于示例讲解。
@key .wui-page 一个CSS类名"wui-page",关键字以"."开头。
@key #wui-pages 一个DOM对象,id为"wui-pages",关键字以"#"开头。

对于模块下的fn,var,class这些类别,如非特别说明,调用时应加WUI前缀,如

WUI.showPage("#pageOrder");
var opts = WUI.options;
var batch = new WUI.batchCall();
batch.commit();

以下函数可不加WUI前缀:

intSort
numberSort
callSvr
callSvrSync
app_alert
app_confirm
app_show

参考wui-name.js模块。

@fn assert(cond, dscr?)

@fn randInt(from, to)

生成指定区间的随机整数。示例:

var i = randInt(1, 10); // 1-10之间的整数,包含1或10

@fn randInt(from, to)

生成随机字符串,包含字母或数字,不包含易混淆的0或O。示例:

var dynCode = randChr(4); // e.g. "9BZ3"

@fn parseQuery(str)

解析url编码格式的查询字符串,返回对应的对象。

if (location.search) {
    var queryStr = location.search.substr(1); // "?id=100&name=abc&val=3.14"去掉"?"号
    var args = parseQuery(queryStr); // {id: 100, name: "abc", val: 3.14}
}

注意:

如果值为整数或小数,则会转成相应类型。如上例中 id为100,不是字符串"100".

@fn tobool(v)

将字符串转成boolean值。除"0", "1"外,还可以支持字符串 "on"/"off", "true"/"false"等。

@fn reloadSite()

重新加载当前页面,但不要#hash部分。

@fn Date.format(fmt?=L)

日期对象格式化字符串。

@param fmt 格式字符串。由以下组成:

yyyy - 四位年,如2008, 1999
yy - 两位年,如 08, 99
mm - 两位月,如 02, 12
dd - 两位日,如 01, 30
HH - 两位小时,如 00, 23
MM - 两位分钟,如 00, 59
SS - 两位秒,如 00, 59

支持这几种常用格式:
L - 标准日期时间,相当于 "yyyy-mm-dd HH:MM:SS"
D - 标准日期,相当于 "yyyy-mm-dd"
T - 标准时间,相当于 "HH:MM:SS"

示例:

var dt = new Date();
var dtStr1 = dt.format("D"); // "2009-10-20"
var dtStr2 = dt.format("yyyymmdd-HHMM"); // "20091020-2038"

@fn Date.addDay(n)

@fn Date.addHours(n)

@fn Date.addMin(n)

@fn Date.addMonth(n)

@fn parseTime(s)

将纯时间字符串生成一个日期对象。

var dt1 = parseTime("10:10:00");
var dt2 = parseTime("10:11");

@fn parseDate(dateStr)

将日期字符串转为日期时间格式。其效果相当于new Date(Date.parse(dateStr)),但兼容性更好(例如在safari中很多常见的日期格式无法解析)

示例:

var dt1 = parseDate("2012-01-01");
var dt2 = parseDate("2012/01/01 20:00:09");
var dt3 = parseDate("2012.1.1 20:00");

支持时区,时区格式可以是"+8", "+08", "+0800", "Z"这些,如

parseDate("2012-01-01T09:10:20.328+0800");
parseDate("2012-01-01T09:10:20Z");

@fn Date.add(sInterval, n)

为日期对象加几天/小时等。参数n为整数,可以为负数。

@param sInterval Enum. 间隔单位. d-天; m-月; y-年; h-小时; n-分; s-秒

示例:

var dt = new Date();
dt.add("d", 1); // 1天后
dt.add("m", 1); // 1个月后
dt.add("y", -1); // 1年前
dt.add("h", 3); // 3小时后
dt.add("n", 30); // 30分钟后
dt.add("s", 30); // 30秒后

@see Date.diff

@fn Date.diff(sInterval, dtEnd)

计算日期到另一日期间的间隔,单位由sInterval指定(具体值列表参见Date.add).

var dt = new Date();
...
var dt2 = new Date();
var days = dt.diff("d", dt2); // 相隔多少天

@see Date.add

@fn getTimeDiffDscr(tm, tm1)

从tm到tm1的时间差描述,如"2分钟前", "3天前"等。

tm和tm1可以为时间对象或时间字符串

@fn WUI.getTmRange(dscr, now?)

根据时间段描述得到[起始时间,结束时间),注意结束时间是开区间(即不包含)。
假设今天是2015-9-9 周三:

getTmRange("本周", "2015-9-9") -> ["2015-9-7"(本周一), "2015-9-14")
getTmRange("上周") -> ["2015-8-31", "2015-9-7")  // 或"前1周"

getTmRange("本月") -> ["2015-9-1", "2015-10-1")
getTmRange("上月") -> ["2015-8-1", "2015-9-1")

getTmRange("今年") -> ["2015-1-1", "2016-1-1") // 或"本年"
getTmRange("去年") -> ["2014-1-1", "2015-1-1") // 或"上年"

getTmRange("本季度") -> ["2015-7-1", "2015-10-1") // 7,8,9三个月
getTmRange("上季度") -> ["2015-4-1", "2015-7-1")

getTmRange("上半年") -> ["2015-1-1", "2015-7-1")
getTmRange("下半年") -> ["2015-7-1", "2016-1-1")

getTmRange("今天") -> ["2015-9-9", "2015-9-10") // 或"本日"
getTmRange("昨天") -> ["2015-9-8", "2015-9-9") // 或"昨日"

getTmRange("前1周") -> ["2015-8-31"(上周一),"2015-9-7"(本周一))
getTmRange("前3月") -> ["2015-6-1", "2015-9-1")
getTmRange("前3天") -> ["2015-9-6", "2015-9-9")

getTmRange("近1周") -> ["2015-9-3","2015-9-10")
getTmRange("近3月") -> ["2015-6-10", "2015-9-10")
getTmRange("近3天") -> ["2015-9-6", "2015-9-10")  // "前3天"+今天

dscr可以是

"近|前|上" N "个"? "小时|日|周|月|年|季度"
"本|今" "小时|日/天|周|月|年|季度"

注意:"近X周"包括今天(即使尚未过完)。

示例:快捷填充

    <td>
        <select class="cboTmRange">
            <option value ="本月">本月</option>
            <option value ="上月">上月</option>
            <option value ="本周">本周</option>
            <option value ="上周">上周</option>
            <option value ="今年">今年</option>
            <option value ="去年">去年</option>
        </select>
    </td>

var txtTmRange = jdlg.find(".cboTmRange");
txtTmRange.change(function () {
    var range = WUI.getTmRange(this.value);
    if (range) {
        WUI.setFormData(jfrm, {tm1: range[0], tm2: range[1]}, {setOnlyDefined: true});
    }
});
// 初始选中
setTimeout(function () {
    txtTmRange.change();
});

@fn setCookie(name, value, days?=30)

设置cookie值。如果只是为了客户端长时间保存值,一般建议使用 setStorage.

@see getCookie

@see delCookie

@see setStorage

@fn getCookie(name)

取cookie值。

@see setCookie

@see delCookie

@fn delCookie(name)

删除一个cookie项。

@see getCookie

@see setCookie

@fn setStorage(name, value, useSession?=false)

使用localStorage存储(或使用sessionStorage存储, 如果useSession=true)。
value可以是简单类型,也可以为数组,对象等,后者将自动在序列化后存储。

如果设置了window.STORAGE_PREFIX, 则键值(name)会加上该前缀.

示例:

setStorage("id", "100");
var id = getStorage("id");
delStorage("id");

示例2:存储对象:

window.STORAGE_PREFIX = "jdcloud_"; // 一般在app.js中全局设置
var obj = {id:10, name:"Jason"};
setStorage("obj", obj);   // 实际存储键值为 "jdcloud_obj"
var obj2 = getStorage("obj");
alert(obj2.name);

@var STORAGE_PREFIX 本地存储的键值前缀

如果指定, 则调用setStorage/getStorage/delStorage时都将自动加此前缀, 避免不同项目的存储项冲突.

@see getStorage

@see delStorage

@fn getStorage(name, useSession?=false)

取storage中的一项。
默认使用localStorage存储,如果useSession=true,则使用sessionStorage存储。

如果浏览器不支持Storage,则使用cookie实现.

@see setStorage

@see delStorage

@fn delStorage(name)

删除storage中的一项。

@see getStorage

@see setStorage

@fn rs2Array(rs)

@param rs= {h=[header], d=[ @row ]} rs对象(RowSet)

@return arr=[ %obj ]

rs对象用于传递表格,包含表头与表内容。
函数用于将服务器发来的rs对象转成数组。

示例:

var rs = {
    h: ["id", "name"], 
    d: [ [100, "Tom"], [101, "Jane"] ] 
};
var arr = rs2Array(rs); 

// 结果为
arr = [
    {id: 100, name: "Tom"},
    {id: 101, name: "Jane"} 
];

@see rs2Hash

@see rs2MultiHash

@fn rs2Hash(rs, key)

@param rs= {h, d} rs对象(RowSet)

@return hash= {key => %obj}

示例:

var rs = {
    h: ["id", "name"], 
    d: [ [100, "Tom"], [101, "Jane"] ] 
};
var hash = rs2Hash(rs, "id"); 

// 结果为
hash = {
    100: {id: 100, name: "Tom"},
    101: {id: 101, name: "Jane"}
};

key可以为一个函数,返回实际key值,示例:

var hash = rs2Hash(rs, function (o) {
    return "USER-" + o.id;
}); 

// 结果为
hash = {
    "USER-100": {id: 100, name: "Tom"},
    "USER-101": {id: 101, name: "Jane"}
};

key函数也可以返回[key, value]数组:

var hash = rs2Hash(rs, function (o) {
    return ["USER-" + o.id, o.name];
}); 

// 结果为
hash = {
    "USER-100": "Tom",
    "USER-101": "Jane"
};

@see rs2Array

@fn rs2MultiHash(rs, key)

数据分组(group by).

@param rs= {h, d} rs对象(RowSet)

@return hash= {key => [ %obj ]}

示例:

var rs = {
    h: ["id", "name"], 
    d: [ [100, "Tom"], [101, "Jane"], [102, "Tom"] ] 
};
var hash = rs2MultiHash(rs, "name");  

// 结果为
hash = {
    "Tom": [{id: 100, name: "Tom"}, {id: 102, name: "Tom"}],
    "Jane": [{id: 101, name: "Jane"}]
};

key也可以是一个函数,返回实际的key值,示例,按生日年份分组:

var rs = {
    h: ["id", "name", "birthday"], 
    d: [ [100, "Tom", "1998-10-1"], [101, "Jane", "1999-1-10"], [102, "Tom", "1998-3-8"] ] 
};
// 按生日年份分组
var hash = rs2MultiHash(rs, function (o) {
    var m = o.birthday.match(/^\d+/);
    return m && m[0];
});

// 结果为
hash = {
    "1998": [{id: 100, name: "Tom", birthday: "1998-10-1"}, {id: 102, name: "Tom", birthday:"1998-3-8"}],
    "1999": [{id: 101, name: "Jane", birthday: "1999-1-10"}]
};

key作为函数,也可返回[key, value]:

var hash = rs2MultiHash(rs, function (o) {
    return [o.name, [o.id, o.birthday]];
});

// 结果为
hash = {
    "Tom": [[100, "1998-10-1"], [102, "1998-3-8"]],
    "Jane": [[101, "1999-1-10"]]
};

@see rs2Hash

@see rs2Array

@fn list2varr(ls, colSep=':', rowSep=',')

将字符串代表的压缩表("v1:v2:v3,...")转成对象数组。

e.g.

var users = "101:andy,102:beddy";
var varr = list2varr(users);
// varr = [["101", "andy"], ["102", "beddy"]];
var arr = rs2Array({h: ["id", "name"], d: varr});
// arr = [ {id: 101, name: "andy"}, {id: 102, name: "beddy"} ];

var cmts = "101\thello\n102\tgood";
var varr = list2varr(cmts, "\t", "\n");
// varr=[["101", "hello"], ["102", "good"]]

@fn objarr2list(objarr, fields, sep=':', sep2=',')

将对象数组转成字符串代表的压缩表("v1:v2:v3,...")。

示例:

var objarr = [
    {id:100, name:'name1', qty:2},
    {id:101, name:'name2', qty:3}
];
var list = objarr2list(objarr, ["id","qty"]);
// 返回"100:2,101:3"

var list2 = objarr2list(objarr, function (e, i) { return e.id + ":" + e.qty; });
// 结果同上

@fn intSort(a, b)

整数排序. 用于datagrid column sorter:

<th data-options="field:'id', sortable:true, sorter:intSort">编号</th>

@fn numberSort(a, b)

小数排序. 用于datagrid column sorter:

<th data-options="field:'score', sortable:true, sorter:numberSort">评分</th>

@fn getAncestor(o, fn)

取符合条件(fn)的对象,一般可使用$.closest替代

@fn appendParam(url, param)

示例:

var url = "http://xxx/api.php";
if (a)
    url = appendParam(url, "a=" + a);
if (b)
    url = appendParam(url, "b=" + b);

appendParam(url, $.param({a:1, b:3}));

支持url中带有"?"或"#",如

var url = "http://xxx/api.php?id=1#order";
appendParam(url, "pay=1"); // "http://xxx/api.php?id=1&pay=1#order";

@fn deleteParam(url, paramName)

示例:

var url = "http://xxx/api.php?a=1&b=3&c=2";
var url1 = deleteParam(url, "b"); // "http://xxx/api.php?a=1&c=2";

var url = "http://server/jdcloud/m2/?logout#me";
var url1 = deleteParam(url, "logout"); // "http://server/jdcloud/m2/?#me"

@fn isWeixin()

当前应用运行在微信中。

@fn isIOS()

当前应用运行在IOS平台,如iphone或ipad中。

@fn isAndroid()

当前应用运行在安卓平台。

@fn parseValue(str)

如果str符合整数或小数,则返回相应类型。

@fn applyTpl(tpl, data)

对模板做字符串替换

var tpl = "<li><p>{name}</p><p>{dscr}</p></li>";
var data = {name: 'richard', dscr: 'hello'};
var html = applyTpl(tpl, data);
// <li><p>richard</p><p>hello</p></li>

@fn delayDo(fn, delayCnt?=3)

设置延迟执行。当delayCnt=1时与setTimeout效果相同。
多次置于事件队列最后,一般3次后其它js均已执行完毕,为idle状态

@fn kvList2Str(kv, sep, sep2)

e.g.

var str = kvList2Str({"CR":"Created", "PA":"Paid"}, ';', ':');
// str="CR:Created;PA:Paid"

@fn parseKvList(kvListStr, sep, sep2, doReverse?) -> kvMap

解析key-value列表字符串,返回kvMap。

示例:

var map = parseKvList("CR:新创建;PA:已付款", ";", ":");
// map: {"CR": "新创建", "PA":"已付款"}

var map = parseKvList("CR:新创建;PA:已付款", ";", ":", true);
// map: {"新创建":"CR", "已付款":"PA"}

sep和sep2支持使用正则式,以下处理结果相同:

var map = parseKvList("CR:新创建;PA:已付款", /[; ]/, /[:=]/);
var map2 = parseKvList("CR=新创建 PA=已付款", /[; ]/, /[:=]/);

@fn Q(str, q?="'")

Q("abc") -> 'abc'
Q("a'bc") -> 'a\'bc'

@fn text2html(str, pics)

将文本或图片转成html,常用于将筋斗云后端返回的图文内容转成html在网页中显示。示例:

var item = {id: 1, name: "商品1", content: "商品介绍内容", pics: "100,102"};
var html = MUI.text2html(item.content, item.pics);
jpage.find("#content").html(html);

文字转html示例:

var html = MUI.text2html("hello\nworld");

生成html为

<p>hello</p>
<p>world</p>

支持简单的markdown格式,如"# ","## "分别表示一二级标题, "- "表示列表(注意在"#"或"-"后面有英文空格):

# 标题1
内容1
# 标题2
内容2

- 列表1
- 列表2

函数可将图片编号列表转成img列表,如:

var html = MUI.text2html(null, "100,102");

生成

<img src="../api.php/att?thumbId=100">
<img src="../api.php/att?thumbId=102">

@fn extendNoOverride(a, b, ...)

var a = {a: 1};
WUI.extendNoOverride(a, {b: 'aa'}, {a: 99, b: '33', c: 'bb'});
// a = {a: 1, b: 'aa', c: 'bb'}

@fn myround(floatValue, digitCnt=6)

与toFixed相比,它返回float数组,不带多余的0位。

myround(114957.15999999, 4) = 114957.16
myround(114957.15999999, 1) = 114957.2

@fn jdModule(name?, fn?)

定义JS模块。这是一个全局函数。

定义一个模块:

jdModule("jdcloud.common", JdcloudCommon);
function JdcloudCommon() {
    var self = this;

    // 对外提供一个方法
    self.rs2Array = rs2Array;
    function rs2Array(rs)
    {
        return ...;
    }
}

获取模块对象:

var mCommon = jdModule("jdcloud.common");
var arr = mCommon.rs2Array(rs);

返回模块映射列表。

var moduleMap = jdModule(); // 返回 { "jdcloud.common": JdcloudCommon, ... }

@fn jdPush(app, handleMsg, opt?={user, url, dataType})

支持出错自动重连的websocket client. 默认连接同域下的jdserver。

示例:

// 返回websocket proxy对象
var ws = jdPush("tighten", function (msg, ws) {
    console.log(msg)
    // 发送消息
    ws.send({ac:"hello"}); // 发送JS对象会自动转JSON发送。
}, opt);

// 关闭
ws.close();

开发环境可配置代理服务器连接jdserver,比如使用Apache,在web根目录下配置.htaccess:
(确保已打开wstunnel模块且允许htaccess文件)

# 连接已部署的websocket server
rewriterule ^jdserver ws://oliveche.com/jdserver [P,L]

# 或连本地websocket server (注意最后/不可少)
# rewriterule ^jdserver ws://127.0.0.1:8081/ [P,L]

jdserver同时支持http和websocket,建议设置为:(注意顺序)

rewriterule ^jdserver/(.+) http://127.0.0.1:8081/$1 [P,L]
rewriterule ^jdserver ws://127.0.0.1:8081/ [P,L]

rewriterule ^jdserver/(.+) http://oliveche.com/jdserver/$1 [P,L]
rewriterule ^jdserver ws://oliveche.com/jdserver/ [P,L]

这样,可以使用基于HTTP的push接口给别的客户端发消息:

callSvr("/jdserver/push", $.noop, {
    app: "app1",
    user: "*", // 表示发给app1中所有人。也可指定`user1`, `user1,user2`, `user*`这样
    msg: {
        ac: "msg1",
        data: "hello"
    }
});
// 返回推送人次数量,示例:2

上面通过msg字段发送js对象。
注意jdPush默认只处理json消息: handleMsg(jsonObj),其它消息只在控制台打印;
想处理其它消息,可调用jdPush时设置opt.dataType='text'

取在线用户:

callSvr("/jdserver/getUsers", { app: "app1" });
// 返回示例: ['user1', 'user2' ]

示例:websocket连接jdserver,然后给自己发消息

jdPush('app1'); // 默认用户为g_data.userInfo.id
callSvr('/jdserver/push', {app: 'app1', user: '*', msg: 'hi'});
// 在控制台可收到hi

@fn getFormData(jo, doGetAll)

取DOM对象中带name属性的子对象的内容, 放入一个JS对象中, 以便手工调用callSvr.

注意:

与setFormData配合使用时, 可以只返回变化的数据.

jf.submit(function () {
    var ac = jf.attr("action");
    callSvr(ac, fn, getFormData(jf));
});

在dialog的onValidate/onOk回调中,由于在显示对话框时自动调用过setFormData,所以用getFormData只返回有修改变化的数据。如果要取所有数据可设置参数doGetAll=true:

var data = WUI.getFormData(jfrm, true);

这也只返回非disabled的组件,如果包括disabled组件的值也需要,可以用

var data = WUI.getFormData(jfrm, "all");

如果在jo对象中存在有name属性的file组件(input[type=file][name]),或指定了属性enctype="multipart/form-data",则调用getFormData会返回FormData对象而非js对象,
再调用callSvr时,会以"multipart/form-data"格式提交数据。一般用于上传文件。
示例:

<div>
    课程文档
    <input name="pdf" type="file" accept="application/pdf">
</div>

或传统地:

<form method="POST" enctype='multipart/form-data'>
    课程文档
    <input name="pdf" type="file" accept="application/pdf">
</form>

如果有多个同名组件(name相同,且非disabled状态),最终值将以最后组件为准。
如果想要以数组形式返回所有值,应在名字上加后缀"[]",示例:

行统计字段: <select name="gres[]" class="my-combobox fields"></select>
行统计字段2: <select name="gres[]" class="my-combobox fields"></select>
列统计字段: <select name="gres2" class="my-combobox fields"></select>
列统计字段2: <select name="gres2" class="my-combobox fields"></select>

取到的结果示例:

{ gres: ["id", "name"], gres2: "name" }

@see setFormData

@fn getFormData_vf(jo)

专门取虚拟字段的值。例如:

<select name="whId" class="my-combobox" data-options="url:..., jd_vField:'whName'"></select>

用WUI.getFormData可取到{whId: xxx},而WUI.getFormData_vf遍历带name属性且设置了jd_vField选项的控件,调用接口getValue_vf(ji)来取其显示值。
因而,为支持取虚拟字段值,控件须定义getValue_vf接口。

<input name="orderType" data-options="jd_vField:'orderType'" disabled>

注意:与getFormData不同,它不忽略有disabled属性的控件。

@see defaultFormItems

@fn formItems(jo, cb)

表单对象遍历。对表单jo(实际可以不是form标签)下带name属性的控件,交给回调cb处理。

注意:

对于checkbox,设置时根据val确定是否选中;取值时如果选中取value属性否则取value-off属性。
缺省value为"on", value-off为空(非标准属性,本框架支持),可以设置:

<input type="checkbox" name="flag" value="1">
<input type="checkbox" name="flag" value="1" value-off="0">

@param cb (ji, name, it) it.getDisabled/setDisabled/getValue/setValue/getShowbox

当cb返回false时可中断遍历。

示例:

WUI.formItems(jdlg.find(".my-fixedField"), function (ji, name, it) {
    var fixedVal = ...
    if (fixedVal || fixedVal == '') {
        it.setReadonly(true);
        var forAdd = beforeShowOpt.objParam.mode == FormMode.forAdd;
        if (forAdd) {
            it.setValue(fixedVal);
        }
    }
    else {
        it.setReadonly(false);
    }
});

@fn getFormItem (ji)

获取对话框上一个组件的访问器。示例,设置名为orderId的组件值:

var ji = jdlg.find("[name=orderId]"); // 或者用 var ji = $(frm.orderId);
var it = WUI.getFormItem(ji); // 或者用 var it = ji.gn()
it.setValue("hello"); // 类似于ji.val("hello"),但支持各种复杂组件

还可以用更简洁的jo.gn,以及支持用链式风格的调用:

var it = jdlg.gn("orderId");
it.val("hello").disabled(true); // 等价于 it.setValue("hello");  it.setDisabled(true);

@see jQuery.fn.gn (name?)

@var getFormItemExt

可通过扩展WUI.getFormItemExt[新类型]来为表单扩展其它类型控件。示例:

WUI.getFormItemExt["myinput"] = function (ji) {
    if (ji.hasClass("myinput"))
        return new MyInputFormItem(ji);
}
function MyInputFormItem(ji) {
    WUI.FormItem.call(this, ji);
}
MyInputFormItem.prototype = $.extend(new WUI.FormItem(), {
    getValue: function () {
        return this.ji.val();
    }
});

@fn jQuery.fn.gn(name?)

按名字访问组件(gn表示getElementByName),返回访问器(iterator),用于对各种组件进行通用操作。

示例:

var it = jdlg.gn("orderId"); 
var v = it.val(); // 取值,相当于 jdlg.find("[name=orderId]").val(); 但兼容my-combobox, wui-combogrid, wui-subobj等复杂组件。
it.val(100); // 设置值
// it.val([100, "ORDR-100"]); // 对于combogrid, 可以传数组,同时设置value和text

var isVisible = it.visible(); // 取是否显示
it.visible(false); // 设置隐藏

var isDisabled = it.disabled();
it.disabled(true);

var isReadonly = it.readonly();
it.readonly(true);

如果未指定name,则以jQuery对象本身作为组件返回访问器。所以也可以这样用:

var jo = jdlg.find("[name=orderId]");
var it = jo.gn(); // name可以省略,表示取自身
it.val(100);

@fn setFormData(jo, data?, opt?)

用于为带name属性的DOM对象设置内容为data[name].
要清空所有内容, 可以用 setFormData(jo), 相当于增强版的 form.reset().

注意:

@param opt {setOrigin?=false, setOnlyDefined?=false}

@param opt.setOrigin 为true时将data设置为数据源, 这样在getFormData时, 只会返回与数据源相比有变化的数据.

缺省会设置该DOM对象数据源为空.

@param opt.setOnlyDefined 设置为true时,只设置form中name在data中存在的项,其它项保持不变;而默认是其它项会清空。

对象关联的数据源, 可以通过 jo.data("origin") 来获取, 或通过 jo.data("origin", newOrigin) 来设置.

示例:

<div id="div1">
    <p>订单描述:<span name="dscr"></span></p>
    <p>状态为:<input type=text name="status"></p>
    <p>金额:<span name="amount"></span>元</p>
</div>

Javascript:

var data = {
    dscr: "筋斗云教程",
    status: "已付款",
    amount: "100"
};
var jo = $("#div1");
var data = setFormData(jo, data); 
$("[name=status]").html("已完成");
var changedData = getFormData(jo); // 返回 { dscr: "筋斗云教程", status: "已完成", amount: "100" }

var data = setFormData(jo, data, {setOrigin: true}); 
$("[name=status]").html("已完成");
var changedData = getFormData(jo); // 返回 { status: "已完成" }
$.extend(jo.data("origin_"), changedData); // 合并变化的部分到数据源.

@see getFormData

@fn loadScript(url, fnOK?, ajaxOpt?)

@param fnOK 加载成功后的回调函数

@param ajaxOpt 传递给$.ajax的额外选项。

默认未指定ajaxOpt时,简单地使用添加script标签机制异步加载。如果曾经加载过,可以重用cache。

注意:再次调用时是否可以从cache中取,是由服务器的cache-control决定的,在web和web/page目录下的js文件一般是禁止缓存的,再次调用时会从服务器再取,若文件无更改,服务器会返回304状态。
这是因为默认我们使用Apache做服务器,在相应目录下.htaccess中配置有缓存策略。

如果指定ajaxOpt,且非跨域,则通过ajax去加载,可以支持同步调用。如果是跨域,仍通过script标签方式加载,注意加载完成后会自动删除script标签。

返回defered对象(与$.ajax类似),可以用 dfd.then() / dfd.fail() 异步处理。

常见用法:

示例:在菜单中加一项“工单工时统计”,动态加载并执行一个JS文件:
store.html中设置菜单:

            <a href="javascript:;" onclick="WUI.loadScript('page/mod_工单工时统计.js')">工单工时统计</a>

page/mod_工单工时统计.js文件中写报表逻辑,mod表示一个JS模块文件,示例:

function show工单工时统计()
{
    DlgReportCond.show(function (data) {
        var queryParams = WUI.getQueryParam({createTm: [data.tm1, data.tm2]});
        var url = WUI.makeUrl("Ordr.query", { res: 'id 工单号, code 工单码, createTm 生产日期, itemCode 产品编码, itemName 产品名称, cate2Name 产品系列, itemCate 产品型号, qty 数量, mh 理论工时, mh1 实际工时', pagesz: -1 });
        WUI.showPage("pageSimple", "工单工时统计!", [url, queryParams, onInitGrid]);
    });
}
show工单工时统计();

如果JS文件修改了,点菜单时可以实时执行最新的内容。

如果要动态加载script,且使用后删除标签(里面定义的函数会仍然保留),建议直接使用$.getScript,它等同于:

loadScript("1.js", {cache: false});

[小技巧]

在index.js/app.js等文件中写代码,必须刷新整个页面才能加载生效。
可以先把代码写在比如 web/test.js 中,这样每次修改后不用大刷新,直接在chrome控制台上加载运行:

WUI.loadScript("test.js")

等改好了再拷贝到真正想放置这块代码的地方。修改已有的框架中函数也可以这样来快速迭代。

@fn evalOptions(_source, ctx?, errPrompt?)

执行JS代码,返回JS对象,示例:

source='{a: 1, b:"hello", c: function () {} }'
前面允许有单行注释,如
source='// comment\n {a: 1, b:"hello", c: function () {} }'

JS代码一般以花括号开头。在用于DOM属性时,也允许没有花括号的这种写法,如:

<div data-options="a:1,b:'hello',c:true"></div>

上例可返回 {a:1, b:'hello', c:true}.

也支持各种表达式及函数调用,如:

<div data-options="getSomeOption()"></div>

更复杂的,如果代码不只是一个选项,前面还有其它JS语句,则返回最后一行语句的值,
最后一行可以是变量、常量、函数调用等,但不可以是花括号开头的选项,对选项须用括号包起来:

function hello() { }
({a: 1, b:"hello", c: hello })

或:

function hello() { }
var ret={a: 1, b:"hello", c: hello }
ret

传入的参数变量ctx可以在代码中使用,用于框架与扩展代码的交互,如:

{a: 1, b:"hello", c: ctx.fn1() }

执行出错时错误显示在控制台。调用者可回调处理错误。

evalOptions(src, null, function (ex) {
    app_alert("文件" + url + "出错: " + ex);
});

注意:受限于浏览器,若使用try { eval(str) } catch(ex) {} 结构会导致原始的错误行号丢失,
为了避免大量代码中有错误时调试困难,在大于1行代码时,不使用try-catch,而是直接抛出错误。

@fn loadJson(url, fnOK, options)

从远程获取JSON结果.
注意: 与$.getJSON不同, 本函数不直接调用JSON.parse解析结果, 而是将返回当成JS代码使用eval执行得到JSON结果再回调fnOK.

示例:

WUI.loadJson("1.js", function (data) {
    // handle json value `data`
});

1.js可以是返回任意JS对象的代码, 如:

// 可以有注释
{
    a: 2 * 3600,
    b: "hello",
    // c: {}
    d: function () {
    },
}

复杂地,可以定义变量、函数,注意最后返回值对象要用小括号把返回对象括起来,如:

var schema = {
    title: "菜单配置",
};

function onReady() {
}

({
    schema: schema,
    onReady: onReady
})

建议用模块方式来写,即用(function() { ... return ... })()把定义内容包起来,这样变量和函数不会污染全局空间:

(function () {
var schema = ...
function onReady() {...}
return {
    schema: schema,
    onReady: onReady
}
})();

不处理结果时, 则该函数与$.getScript效果类似.

@param options 参考$.ajax参数

@options.async 默认为异步,设置{async:false}表示同步调用。

@see evalOptions

@fn loadCss(url)

动态加载css文件, 示例:

WUI.loadCss("lib/bootstrap.min.css");

@fn setDateBox(jo, defDateFn?)

设置日期框, 如果输入了非法日期, 自动以指定日期(如未指定, 用当前日期)填充.

setDateBox($("#txtComeDt"), function () { return genDefVal()[0]; });

@fn setTimeBox(jo, defTimeFn?)

设置时间框, 如果输入了非法时间, 自动以指定时间(如未指定, 用当前时间)填充.

setTimeBox($("#txtComeTime"), function () { return genDefVal()[1]; });

@fn waitFor(deferredObj)

用于简化异步编程. 可将不易读的回调方式改写为易读的顺序执行方式.

var dfd = $.getScript("http://...");
function onSubmit()
{
    dfd.then(function () {
        foo();
        bar();
    });
}

可改写为:

function onSubmit()
{
    if (waitFor(dfd)) return;
    foo();
    bar();
}

@fn rgb(r,g,b)

生成"#112233"形式的颜色值.

rgb(255,255,255) -> "#ffffff"

@fn rgb2hex(rgb)

将jquery取到的颜色转成16进制形式,如:"rgb(4, 190, 2)" -> "#04be02"

示例:

var color = rgb2hex( $(".mui-container").css("backgroundColor") );

@fn jQuery.fn.jdata(val?)

和使用$.data()差不多,更好用一些. 例:

$(o).jdata().hello = 100;
$(o).jdata({hello:100, world:200});

@fn compressImg(img, cb, opt)

通过限定图片大小来压缩图片,用于图片预览和上传。
不支持IE8及以下版本。

函数cb的回调参数: picData={b64src,blob,w,h,w0,h0,quality,name,mimeType,size0,size,b64size,info}

b64src为base64格式的Data URL, 如 "data:image/jpeg;base64,/9j/4AAQSk...", 用于给image或background-image赋值显示图片;

可以赋值给Image.src:

var img = new Image();
img.src = picData.b64src;

$("<div>").css("background-image", "url(" + picData.b64src + ")");

blob用于放到FormData中上传:

fd.append('file', picData.blob, picData.name);

其它picData属性:

[预览和上传示例]

HTML:

<form action="upfile.php">
    <div class="img-preview"></div>
    <input type="file" /><br/>
    <input type="submit" >
</form>

用picData.b64src来显示预览图,并将picData保存在img.picData_属性中,供后面上传用。

var jfrm = $("form");
var jpreview = jfrm.find(".img-preview");
var opt = {maxSize:1280};
jfrm.find("input[type=file]").change(function (ev) {
    $.each(this.files, function (i, fileObj) {
        compressImg(fileObj, function (picData) {
            $("<img>").attr("src", picData.b64src)
                .prop("picData_", picData)
                .appendTo(jpreview);
            //$("<div>").css("background-image", "url("+picData.b64src+")").appendTo(jpreview);
        }, opt);
    });
    this.value = "";
});

上传picData.blob到服务器

jfrm.submit(function (ev) {
    ev.preventDefault();

    var fd = new FormData();
    var idx = 1;
    jpreview.find("img").each(function () {
        // 名字要不一样,否则可能会覆盖
        fd.append('file' + idx, this.picData_.blob, this.picData_.name);
        ++idx;
    });

    $.ajax({
        url: jfrm.attr("action"),
        data: fd,
        processData: false,
        contentType: false,
        type: 'POST',
        // 允许跨域调用
        xhrFields: {
            withCredentials: true
        },
        success: cb
    });
    return false;
});

参考:JIC.js (https://github.com/brunobar79/J-I-C)

TODO: 用完后及时释放内存,如调用revokeObjectURL等。

@fn triggerAsync(jo, ev, paramArr)

触发含有异步操作的事件,在异步事件完成后继续。兼容同步事件处理函数,或多个处理函数中既有同步又有异步。
返回Deferred对象,或false表示要求取消之后操作。

@param ev 事件名,或事件对象$.Event()

示例:以事件触发方式调用jo的异步方法submit:

var dfd = WUI.triggerAsync(jo, 'submit');
if (dfd === false)
    return;
dfd.then(doNext);

function doNext() { }

jQuery对象这样提供异步方法:triggerAsync会用事件对象ev创建一个dfds数组,将Deferred对象存入即可支持异步调用。

jo.on('submit', function (ev) {
    var dfd = $.ajax("upload", ...);
    if (ev.dfds)
        ev.dfds.push(dfd);
});

@fn $.Deferred

@alias Promise

兼容Promise的接口,如then/catch/finally

@fn makeTree(arr, idField="id", fatherIdField="fatherId", childrenField="children")

将array转成tree. 注意它会修改arr(添加children属性),但返回新的数组。

var ret = WUI.makeTree([
    {id: 1},
    {id: 2, fatherId: 1},
    {id: 3, fatherId: 2},
    {id: 4, fatherId: 1}
]);

结果:

ret = [
    {id: 1, children:  [
        {id: 2, fatherId: 1, children:  [
            {id: 3, fatherId: 2},
        ],
        {id: 4, fatherId: 1}
    ]
]

@fn evalAttr(jo, name, ctx?)

返回一个属性做eval后的js值。

示例:读取一个对象值:

var opt = evalAttr(jo, "data-opt");

<div data-opt="{id:1, name:\"data1\"}"><div>

考虑兼容性,也支持忽略括号的写法,

<div data-opt="id:1, name:\"data1\""><div>

读取一个数组:

var arr = evalAttr(jo, "data-arr");

<div data-arr="['aa', 'bb']"><div>

读取一个函数名(或变量):

var fn = evalAttr(jo, "mui-initfn");

<div mui-initfn="initMyPage"><div>

@see evalOptions

@fn app_abort()

中止之后的调用, 直接返回.

@class DirectReturn

直接返回. 用法:

throw new DirectReturn();

可直接调用app_abort();

@fn windowOnError()

框架自动在脚本出错时弹框报错,并上报服务端(syslog).
可通过WUI.options.skipErrorRegex忽略指定错误。

@var m_enhanceFn

@fn enhanceWithin(jparent)

@fn getOptions(jo, defVal?, ctx?)

第一次调用,根据jo上设置的data-options属性及指定的defVal初始化,或为{}
存到jo.prop("muiOptions")上。之后调用,直接返回该属性。

通过指定ctx, 可以为属性计算时添加可访问接口, 示例: (eol1Sn属性是需要取下面input中的值)

<button type="button" class="btnPlcNotify" data-options="newItemFlag:1, eol1Sn:ctx.eolSn">Move In from eol-1</button>
<input id="eolSn" value="sn1"> 

取值:

var data = WUI.getOptions($(this), null, {
    eol1Sn: jpage.find("eolSn").val()
}

当然ctx也可以做成个funciton比如:

<button type="button" class="btnPlcNotify" data-options="newItemFlag:1, eol1Sn:ctx('#eolSn').val()">Move In from eol-1</button>
<input id="eolSn" value="sn1"> 

var data = WUI.getOptions($(this), null, function (ref) {
    return jpage.find(ref);
});

@see evalAttr

@fn getQueryCond(kvList)

@var queryHint 查询用法提示

@param kvList {key=>value}, 键值对,值中支持操作符及通配符。也支持格式 [ [key, value] ], 这时允许key有重复。

根据kvList生成BPQ协议定义的{obj}.query的cond参数。

例如:

var kvList = {phone: "13712345678", id: ">100", addr: "上海*", picId: "null"};
WUI.getQueryCond(kvList);

有多项时,每项之间以"AND"相连,以上定义将返回如下内容:

"phone='13712345678' AND id>100 AND addr LIKE '上海*' AND picId IS NULL"

示例二:

var kvList = [ ["phone", "13712345678"], ["id", ">100"], ["addr", "上海*"], ["picId", "null"] ];
WUI.getQueryCond(kvList); // 结果同上。

设置值时,支持以下格式:

支持and/or查询,但不支持在其中使用括号:

以下表示的范围相同:

{k1:'1-5,7-10', k2:'1-10 and <>6'}

符号优先级依次为:"-"(类似and) ","(类似or) and or

在详情页对话框中,切换到查找模式,在任一输入框中均可支持以上格式。

(v5.5) value支持用数组表示范围(前闭后开区间),特别方便为起始、结束时间生成条件:

var cond = getQueryCond({tm: ["2019-1-1", "2020-1-1"]}); // 生成 "tm>='2019-1-1' AND tm<'2020-1-1'"
var cond = getQueryCond({tm: [null, "2020-1-1"]}); // 生成 "tm<'2020-1-1'"。数组中任一值为null或''都一样会被忽略。
var cond = getQueryCond({tm: [null, null]); // 返回空串''

@see getQueryParam

@see getQueryParamFromTable 获取datagrid的当前查询参数

@see doFind

(v5.5) 支持在key中包含查询提示。如"code/s"表示不要自动猜测数值区间或日期区间。
比如输入'126231-191024'时不会当作查询126231到191024的区间。

(v6) 日期、时间字段查询时,可使用WUI.getTmRange函数支持的时间区间如"今天","本周","本月", "今年", "近3天(小时|周|月|季度|年)”,"前3天(小时|周|月|季度|年)”等。

@see wui-find-hint

@fn getQueryParam(kvList)

根据键值对生成BQP协议中{obj}.query接口需要的cond参数.
{cond: WUI.getQueryCond(kvList) }

示例:

WUI.getQueryParam({phone: '13712345678', id: '>100'})
返回
{cond: "phone='13712345678' AND id>100"}

@see getQueryCond

@see getQueryParamFromTable 获取datagrid的当前查询参数

@fn doSpecial(jo, filter, fn, cnt=5, interval=2s)

连续5次点击某处,每次点击间隔不超过2s, 执行隐藏动作。

例:
// 连续5次点击当前tab标题,重新加载页面. ev为最后一次点击事件.
var self = WUI;
self.doSpecial(self.tabMain.find(".tabs-header"), ".tabs-selected", function (ev) {
self.reloadPage();
self.reloadDialog(true);

    // 弹出菜单
    //jmenu.menu('show', {left: ev.pageX, top: ev.pageY});
    return false;
});

连续3次点击对话框中的字段标题,触发查询:

WUI.doSpecial(jdlg, ".wui-form-table td", fn, 3);

@fn execCopy(text)

复制到剪贴板。

@var lastError = ctx

出错时,取出错调用的上下文信息。

ctx: {ac, tm, tv, ret}

@var disableBatch ?= false

设置为true禁用batchCall, 仅用于内部测试。

@var m_curBatch

当前batchCall对象,用于内部调试。

@var mockData 模拟调用后端接口。

在后端接口尚无法调用时,可以配置MUI.mockData做为模拟接口返回数据。
调用callSvr时,会直接使用该数据,不会发起ajax请求。

mockData={ac => data/fn}

mockData中每项可以直接是数据,也可以是一个函数:fn(param, postParam)->data

例:模拟"User.get(id)"和"User.set()(key=value)"接口:

var user = {
    id: 1001,
    name: "孙悟空",
};
MUI.mockData = {
    // 方式1:直接指定返回数据
    "User.get": [0, user],

    // 方式2:通过函数返回模拟数据
    "User.set": function (param, postParam) {
        $.extend(user, postParam);
        return [0, "OK"];
    }
}

// 接口调用:
var user = callSvrSync("User.get");
callSvr("User.set", {id: user.id}, function () {
    alert("修改成功!");
}, {name: "大圣"});

实例详见文件 mockdata.js。

在mockData的函数中,可以用this变量来取ajax调用参数。
要取HTTP动词可以用this.type,值为GET/POST/PATCH/DELETE之一,从而可模拟RESTful API.

可以通过MUI.options.mockDelay设置模拟调用接口的网络延时。

@see options.mockDelay

模拟数据可直接返回[code, data]格式的JSON数组,框架会将其序列化成JSON字符串,以模拟实际场景。
如果要查看调用与返回数据日志,可在浏览器控制台中设置 MUI.options.logAction=true,在控制台中查看日志。

如果设置了MUI.callSvrExt,调用名(ac)中应包含扩展(ext)的名字,例:

MUI.callSvrExt['zhanda'] = {...};
callSvr(['token/get-token', 'zhanda'], ...);

要模拟该接口,应设置

MUI.mockData["zhanda:token/get-token"] = ...;

@see callSvrExt

也支持"default"扩展,如:

MUI.callSvrExt['default'] = {...};
callSvr(['token/get-token', 'default'], ...);
或
callSvr('token/get-token', ...);

要模拟该接口,可设置

MUI.mockData["token/get-token"] = ...;

@key $.ajax

@key ajaxOpt.jdFilter 禁用返回格式合规检查.

以下调用, 如果1.json符合[code, data]格式, 则只返回处理data部分; 否则将报协议格式错误:

$.ajax("1.json", {dataType: "json"})
$.get("1.json", null, console.log, "json")
$.getJSON("1.json", null, console.log)

对于ajax调用($.ajax,$.get,$.post,$.getJSON等), 若明确指定dataType为"json"或"text", 且未指定jdFilter为false,
则框架按筋斗云返回格式即[code, data]来处理只返回data部分, 不符合该格式, 则报协议格式错误.

以下调用未指定dataType, 或指定了jdFilter=false, 则不会应用筋斗云协议格式:

$.ajax("1.json")
$.get("1.json", null, console.log)
$.ajax("1.json", {jdFilter: false}) // jdFilter选项明确指定了不应用筋斗云协议格式

@fn enterWaiting(ctx?)

@param ctx {ac, tm, tv?, tv2?, noLoadingImg?}

@fn leaveWaiting(ctx?)

@fn defDataProc(rv)

@param rv BQP协议原始数据,如 "[0, {id: 1}]",一般是字符串,也可以是JSON对象。

@return data 按接口定义返回的数据对象,如 {id: 1}. 如果返回值===RV_ABORT,调用函数应直接返回,不回调应用层。

注意:如果callSvr设置了noex:1选项,则当调用失败时返回false。

@fn getBaseUrl()

取服务端接口URL对应的目录。可用于拼接其它服务端资源。
相当于dirname(MUI.options.serverUrl);

例如:

serverUrl为"../jdcloud/api.php" 或 "../jdcloud/",则MUI.baseUrl返回 "../jdcloud/"
serverUrl为"http://myserver/myapp/api.php" 或 "http://myserver/myapp/",则MUI.baseUrl返回 "http://myserver/myapp/"

@fn makeUrl(action, params?)

生成对后端调用的url.

var params = {id: 100};
var url = MUI.makeUrl("Ordr.set", params);

注意:函数返回的url是字符串包装对象,可能含有这些属性:{makeUrl=true, action?, params?}
这样可通过url.action得到原始的参数。

支持callSvr扩展,如:

var url = MUI.makeUrl('zhanda:login');

(deprecated) 为兼容旧代码,action可以是一个数组,在WUI环境下表示对象调用:

WUI.makeUrl(['Ordr', 'query']) 等价于 WUI.makeUrl('Ordr.query');

在MUI环境下表示callSvr扩展调用:

MUI.makeUrl(['login', 'zhanda']) 等价于 MUI.makeUrl('zhanda:login');

特别地, 如果action是相对路径, 或是'.php'文件, 则不会自动拼接WUI.options.serverUrl:

callSvr("./1.json"); // 如果是callSvr("1.json") 则url可能是 "../api.php/1.json"这样.
callSvr("./1.php");

@see callSvrExt

@fn callSvr(ac, [params?], fn?, postParams?, userOptions?) -> deferredObject

1 调用监控

2 文件上传支持(FormData)

3 callSvr扩展

4 适配RESTful API

5 ES6支持:jQuery的$.Deferred兼容Promise接口 / 使用await

6 直接取json类文件

7 获取HTTP返回状态码和响应头

@param ac String. action, 交互接口名. 也可以是URL(比如由makeUrl生成)

@param params Object. URL参数(或称HTTP GET参数)

@param postParams Object. POST参数. 如果有该参数, 则自动使用HTTP POST请求(postParams作为POST内容), 否则使用HTTP GET请求.

@param fn Function(data). 回调函数, data参考该接口的返回值定义。

@param userOptions 用户自定义参数, 会合并到$.ajax调用的options参数中.可在回调函数中用"this.参数名"引用.

常用userOptions:

指定contentType和设置自定义HTTP头(headers)示例:

var opt = {
    contentType: "text/xml",
    headers: {
        Authorization: "Basic aaa:bbb"
    }
};
callSvr("hello", $.noop, "<?xml version='1.0' encoding='UTF-8'?><return><code>0</code></return>", opt);

想为ajax选项设置缺省值,可以用callSvrExt中的beforeSend回调函数,也可以用$.ajaxSetup,
但要注意:ajax的dataFilter/beforeSend选项由于框架已用,最好不要覆盖。

@see callSvrExt[].beforeSend (opt) 为callSvr选项设置缺省值

@return deferred对象,在Ajax调用成功后回调。

例如,

var dfd = callSvr(ac, fn1);
dfd.then(fn2);

function fn1(data) {}
function fn2(data) {}

在接口调用成功后,会依次回调fn1, fn2. 在回调函数中this表示ajax参数。例如:

callSvr(ac, function (data) {
    // 可以取到传入的参数。
    console.log(this.key1);
}, null, {key1: 'val1'});

(v5.4) 支持失败时回调:

var dfd = callSvr(ac);
dfd.fail(function (data) {
    console.log('error', data);
    console.log(this.ctx_.ret); // 和设置选项{noex:1}时回调中取MUI.lastError.ret 或 this.lastError相同。
});

@key callSvr.noex 调用接口时忽略出错,可由回调函数fn自己处理错误。

当后端返回错误时, 回调fn(false)(参数data=false). 可通过 MUI.lastError.ret 或 this.lastError 取到返回的原始数据。

示例:

callSvr("logout");
callSvr("logout", api_logout);
function api_logout(data) {}

callSvr("login", api_login);
function api_login(data) {}

callSvr("info/hotline.php", {q: '大众'}, api_hotline);
function api_hotline(data) {}

// 也可使用makeUrl生成的URL如:
callSvr(MUI.makeUrl("logout"), api_logout);
callSvr(MUI.makeUrl("logout", {a:1}), api_logout);

callSvr("User.get", function (data) {
    if (data === false) { // 仅当设置noex且服务端返回错误时可返回false
        // var originalData = MUI.lastError.ret; 或
        // var originalData = this.lastError;
        return;
    }
    foo(data);
}, null, {noex:1});

@see lastError 出错时的上下文信息

1 调用监控

框架会自动在ajaxOption中增加ctx_属性,它包含 {ac, tm, tv, tv2, ret} 这些信息。
当设置MUI.options.logAction=1时,将输出这些信息。

2 文件上传支持(FormData)

callSvr支持FormData对象,可用于上传文件等场景。示例如下:

@key example-upload

HTML:

file: <input id="file1" type="file" multiple>
<button type="button" id="btn1">upload</button>

JS:

jpage.find("#btn1").on('click', function () {
    var fd = new FormData();
    $.each(jpage.find('#file1')[0].files, function (i, e) {
        fd.append('file' + (i+1), e);
    });
    callSvr('upload', api_upload, fd);

    function api_upload(data) { ... }
});

3 callSvr扩展

@key callSvrExt

当调用第三方API时,也可以使用callSvr扩展来代替$.ajax调用以实现:

例:合作方接口使用HTTP协议,格式如(以生成token调用为例)

http://<Host IP Address>:<Host Port>/lcapi/token/get-token?user=用户名&password=密码

返回格式为:{code, msg, data}

成功返回:

{
    "code":"0",
    "msg":"success",
    "data":[ { "token":"xxxxxxxxxxxxxx" } ]
}

失败返回:

{
    "code":"4001",
    "msg":"invalid username or password",
    "data":[]
}

callSvr扩展示例:

MUI.callSvrExt['zhanda'] = {
    makeUrl: function(ac, param) {
        // 只需要返回接口url即可,不必拼接param
        return 'http://hostname/lcapi/' + ac;
    },
    dataFilter: function (data) {
        if ($.isPlainObject(data) && data.code !== undefined) {
            if (data.code == 0)
                return data.data;
            if (this.noex)
                return false;
            app_alert("操作失败:" + data.msg, "e");
        }
        else {
            app_alert("服务器通讯协议异常!", "e"); // 格式不对
        }
    }
};

在调用时,ac参数使用"{扩展名}:{调用名}"的格式:

callSvr('zhanda:token/get-token', {user: 'test', password: 'test123'}, function (data) {
    console.log(data);
});

旧的调用方式ac参数使用数组,现在已不建议使用:

callSvr(['token/get-token', 'zhanda'], ...);

@key callSvrExt[].makeUrl (ac, param)

根据调用名ac生成url, 注意无需将param放到url中。

注意:
对方接口应允许JS跨域调用,或调用方支持跨域调用。

@key callSvrExt[].dataFilter (data) = null/false/data

对调用返回数据进行通用处理。返回值决定是否调用callSvr的回调函数以及参数值。

callSvr(ac, callback);

当返回false时,应用层可以通过MUI.lastError.ret来获取服务端返回数据。

@see lastError 出错时的上下文信息

@key callSvrExt['default']

(支持版本: v3.1)
如果要修改callSvr缺省调用方法,可以改写 MUI.callSvrExt['default']。示例:

MUI.callSvrExt['default'] = {
    makeUrl: function(ac) {
        return '../api.php/' + ac;
    },
    dataFilter: function (data) {
        var ctx = this.ctx_ || {};
        if (data && $.isArray(data) && data.length >= 2 && typeof data[0] == "number") {
            if (data[0] == 0)
                return data[1];

            if (this.noex)
            {
                return false;
            }

            if (data[0] == E_NOAUTH) {
                // 如果支持自动重登录
                //if (MUI.tryAutoLogin()) {
                //  $.ajax(this);
                //}
                // 不支持自动登录,则跳转登录页
                MUI.popPageStack(0);
                MUI.showLogin();
                return;
            }
            else if (data[0] == E_AUTHFAIL) {
                app_alert("验证失败,请检查输入是否正确!", "e");
                return;
            }
            else if (data[0] == E_ABORT) {
                console.log("!!! abort call");
                return;
            }
            logError();
            app_alert("操作失败:" + data[1], "e");
        }
        else {
            logError();
            app_alert("服务器通讯协议异常!", "e"); // 格式不对
        }

        function logError()
        {
            console.log("failed call");
            console.log(ctx);
        }
    }
};

@key callSvrExt[].beforeSend (opt) 为callSvr或$.ajax选项设置缺省值

如果有ajax选项想设置,可以使用beforeSend回调,例如POST参数使用JSON格式:

MUI.callSvrExt['default'] = {
    beforeSend: function (opt) {
        // 示例:设置contentType
        if (opt.contentType == null) {
            opt.contentType = "application/json;charset=utf-8";
        }
        // 示例:添加HTTP头用于认证
        if (g_data.auth) {
            if (opt.headers == null)
                opt.headers = {};
            opt.headers["Authorization"] = "Basic " + g_data.auth;
        }
    }
}

可以从opt.ctx中取到{ac, ext, noex, dfd}等值(如opt.ctx.ac),可以从opt.url中取到{ac, params}值。

如果要设置请求的HTTP headers,可以用opt.headers = {header1: "value1", header2: "value2"}.
更多选项参考jquery文档:jQuery.ajax的选项。

4 适配RESTful API

接口示例:更新订单

PATCH /orders/{ORDER_ID}

调用成功仅返回HTTP状态,无其它内容:"200 OK" 或 "204 No Content"
调用失败返回非2xx的HTTP状态及错误信息,无其它内容,如:"400 bad id"

为了处理HTTP错误码,应设置:

MUI.callSvrExt["default"] = {
    beforeSend: function (opt) {
        opt.handleHttpError = true;
    },
    dataFilter: function (data) {
        var ctx = this.ctx_;
        if (ctx && ctx.status) {
            if (this.noex)
                return false;
            app_alert(ctx.statusText, "e");
            return;
        }
        return data;
    }
}

如果接口在出错时,返回固定格式的错误对象如{code, message},可以这样处理:

MUI.callSvrExt["default"] = {
    beforeSend: function (opt) {
        opt.handleHttpError = true;
    },
    dataFilter: function (data) {
        var ctx = this.ctx_;
        if (ctx && ctx.status) {
            if (this.noex)
                return false;
            if (data && data.message) {
                app_alert(data.message, "e");
            }
            else {
                app_alert("操作失败: 服务器错误. status=" + ctx.status + "-" + ctx.statusText, "e");
            }
            return;
        }
        return data;
    }
}

调用接口时,HTTP谓词可以用callSvr的userOptions中给定,如:

callSvr("orders/" + orderId, fn, postParam, {type: "PATCH"});

这种方式简单,但因调用名ac是变化的,不易模拟接口。
如果要模拟接口,可以保持调用名ac不变,像这样调用:

callSvr("orders/{id}", {id: orderId}, fn, postParam, {type: "PATCH"});

于是可以这样做接口模拟:

MUI.mockData = {
    "orders/{id}": function (param, postParam) {
        var ret = "OK";
        // 获取资源
        if (this.type == "GET") {
            ret = orders[param.id];
        }
        // 更新资源
        else if (this.type == "PATCH") {
            $.extend(orders[param.id], postParam);
        }
        // 删除资源
        else if (this.type == "DELETE") {
            delete orders[param.id];
        }
        return [0, ret];
    }
};

不过这种写法需要适配,以生成正确的URL,示例:

MUI.callSvrExt["default"] = {
    makeUrl: function (ac, param) {
        ac = ac.replace(/\{(\w+)\}/g, function (m, m1) {
            var ret = param[m1];
            assert(ret != null, "缺少参数");
            delete param[m1];
            return ret;
        });
        return "./api.php/" + ac;
    }
}

5 ES6支持:jQuery的$.Deferred兼容Promise接口 / 使用await

支持Promise/Deferred编程风格:

var dfd = callSvr("...");
dfd.then(function (data) {
    console.log(data);
})
.catch(function (err) {
    app_alert(err);
})
.finally(...)

支持catch/finally等Promise类接口。接口逻辑失败时,dfd.reject()触发fail/catch链。

支持await编程风格,上例可写为:

// 使用await时callSvr调用失败是无法返回的,加{noex:1}选项可让失败时返回false
var rv = callSvr("...", $.noop, null, {noex:1});
if (rv === false) {
    // 失败逻辑 dfd.catch. 取错误信息用WUI.lastError={ac, tm, tv, ret}
    console.log(WUI.lastError.ret)
}
else {
    // 成功逻辑 dfd.then
}
// finally逻辑

示例:

let rv = await callSvr("Ordr.query", {res:"count(*) cnt", fmt:"one"})
let cnt = rv.cnt

6 直接取json类文件

(v5.5) 如果ac是调用相对路径, 则直接当成最终路径, 不做url拼接处理:

callSvr("./1.json"); // 如果是callSvr("1.json") 则实际url可能是 "../api.php/1.json"这样.
callSvr("../1.php");

相当于调用

$.ajax("../1.php", {dataType: "json", success: callback})
或
$.getJSON("../1.php", callback);

注意下面调用未指定dataType, 不会按筋斗云协议格式处理:

$.ajax("../1.php", {success: callback})

7 获取HTTP返回状态码和响应头

在回调中获取this.xhr_对象(即$.ajax()返回对象, 详细可参考$.ajax手册), 通过它的getResponseHeader()方法取响应头, status属性来获取状态码, 示例

callSvr("User.query", function (data) {
    console.log("status", this.xhr_.status);
    var v = this.xhr_.getResponseHeader("X-Powered-By"); // header名字不分区大小写,用"x-powered-by"也可以
    console.log(v);
});

当然也可以通过返回的dfd对象来操作:

rv = callSvr("User.query");
rv.then(function (data) {
    console.log("status", this.xhr_.status);
});

@see $.ajax

@fn callSvrSync(ac, [params?], fn?, postParams?, userOptions?)

@return data 原型规定的返回数据

同步模式调用callSvr.

@see callSvr

@fn setupCallSvrViaForm($form, $iframe, url, fn, callOpt)

该方法已不建议使用。上传文件请用FormData。

@see example-upload callSvr

@param $iframe 一个隐藏的iframe组件.

@param callOpt 用户自定义参数. 参考callSvr的同名参数. e.g. {noex: 1}

一般对后端的调用都使用callSvr函数, 但像上传图片等操作不方便使用ajax调用, 因为要自行拼装multipart/form-data格式的请求数据.
这种情况下可以使用form的提交和一个隐藏的iframe来实现类似的调用.

先定义一个form, 在其中放置文件上传控件和一个隐藏的iframe. form的target属性设置为iframe的名字:

<form data-role="content" action="upload" method=post enctype="multipart/form-data" target="ifrUpload">
    <input type=file name="file[]" multiple accept="image/*">
    <input type=submit value="上传">
    <iframe id='ifrUpload' name='ifrUpload' style="display:none"></iframe>
</form>

然后就像调用callSvr函数一样调用setupCallSvrViaForm:

var url = MUI.makeUrl("upload", {genThumb: 1});
MUI.setupCallSvrViaForm($frm, $frm.find("iframe"), url, onUploadComplete);
function onUploadComplete(data) 
{
    alert("上传成功");
}

@class batchCall(opt?={useTrans?=0})

批量调用。将若干个调用打包成一个特殊的batch调用发给服务端。
注意:

示例:

var batch = new MUI.batchCall();
callSvr("Family.query", {res: "id,name"}, api_FamilyQuery);
callSvr("User.get", {res: "id,phone"}, api_UserGet);
batch.commit();

以上两条调用将一次发送到服务端。
在批处理中,默认每条调用是一个事务,如果想把批处理中所有调用放到一个事务中,可以用useTrans选项:

var batch = new MUI.batchCall({useTrans: 1});
callSvr("Attachment.add", api_AttAdd, {path: "path-1"});
callSvr("Attachment.add", api_AttAdd, {path: "path-2"});
batch.commit();

在一个事务中,所有调用要么成功要么都取消。
任何一个调用失败,会导致它后面所有调用取消执行,且所有已执行的调用会回滚。

参数中可以引用之前结果中的值,引用部分需要用"{}"括起来,且要在opt.ref参数中指定哪些参数使用了引用:

MUI.useBatchCall();
callSvr("..."); // 这个返回值的结果将用于以下调用
callSvr("Ordr.query", {
    res: "id,dscr",
    status: "{$-1.status}",  // 整体替换,结果可以是一个对象
    cond: "id>{$-1.id}" // 部分替换,其结果只能是字符串
}, api_OrdrQuery, {
    ref: ["status", "cond"] // 须在ref中指定需要处理的key
});

特别地,当get/post整个是一个字符串时,直接整体替换,无须在ref中指定,如:

callSvr("Ordr.add", $.noop, "{$-1}", {contentType:"application/json"});

以下为引用格式示例:

{$1} // 第1次调用的结果。
{$-1} // 前1次调用的结果。
{$-1.path} // 取前一次调用结果的path属性
{$1[0]} // 取第1次调用结果(是个数组)的第0个值。
{$1[0].amount}
{$-1.price * $-1.qty} // 可以做简单的数值计算

如果值计算失败,则当作"null"填充。

综合示例:

MUI.useBatchCall();
callSvr("Ordr.completeItem", $.noop, {itemId:1})
callSvr("Ordr.completeItem", $.noop, {itemId:2, qty:2})
callSvr("Ordr.calc", $.noop, {items:["{$1}", "{$2}"]}, {contentType:"application/json", ref:["items"] });
callSvr("Ordr.add", $.noop, "{$3}", {contentType:"application/json"});

@see useBatchCall

@see disableBatch

@see m_curBatch

@fn useBatchCall(opt?={useTrans?=0}, tv?=0)

之后的callSvr调用都加入批量操作。例:

MUI.useBatchCall();
callSvr("Family.query", {res: "id,name"}, api_FamilyQuery);
callSvr("User.get", {res: "id,phone"}, api_UserGet);

可指定多少毫秒以内的操作都使用批处理,如10ms内:

MUI.useBatchCall(null, 10);

如果MUI.disableBatch=true, 该函数不起作用。

@see batchCall

@see disableBatch

@fn isBatchMode()

是否批量操作模式(即是否按住Ctrl键操作)。

@fn getRow(jtbl) -> row

用于列表中选择一行来操作,返回该行数据。如果未选则报错,返回null。

var row = WUI.getRow(jtbl);
if (row == null)
    return;

@fn isTreegrid(jtbl)

判断是treegrid还是datagrid。
示例:

var datagrid = WUI.isTreegrid(jtbl)? "treegrid": "datagrid";
var opt = jtbl[datagrid]("options");

@fn reload(jtbl, url?, queryParams?, doAppendFilter?)

刷新数据表,或用指定查询条件重新查询。

url和queryParams都可以指定查询条件,url通过makeUrl指定查询参数,它是基础查询一般不改变;
queryParams在查询对话框做查询时会被替换、或是点Ctrl-刷新时会被清除;如果doAppendFilter为true时会叠加之前的查询条件。
在明细对话框上三击字段标题可查询,按住Ctrl后三击则是追加查询模式。

@fn reloadTmp(jtbl, url?, queryParams?)

临时reload一下,完事后恢复原url

@fn reloadRow(jtbl, rowData?)

@param rowData 通过原始数据指定行,可通过WUI.getRow(jtbl)获取当前所选择的行数据。

rowData如果未指定,则使用当前选择的行。

示例:

var row = WUI.getRow(jtbl);
if (row == null)
    return;
...
WUI.reloadRow(jtbl, row);

如果要刷新整个表,可用WUI.reload(jtbl)。
刷新整个页面可用WUI.reloadPage(),但一般只用于调试,不用于生产环境。

返回Deferred对象,表示加载完成。

@fn showPage(pageName, showPageOpt?={title, target, pageFilter}, paramArr?=[showPageOpt])

1 showPageOpt.pageFilter: (v6) 指定列表页过滤条件(PAGE_FILTER)

2 (v6) 返回deferred对象

3 showPageOpt.target: 指定显示在哪个tabs中

@alias showPage (pageName, title?, paramArr?)

新页面以title作为id。
注意:每个页面都是根据pages下相应pageName复制出来的,显示在一个新的tab页中。相同的title当作同一页面。
初始化函数由page上的my-initfn属性指定。

page定义示例:

<div id="my-pages" style="display:none">
    <div class="pageHome" title="首页" my-initfn="initPageHome"></div>
</div>

page调用示例:

WUI.showPage("pageHome");
WUI.showPage("pageHome", "我的首页"); // 默认标题是"首页",这里指定显示标题为"我的首页"。
WUI.showPage("pageHome", {title: "我的首页"}); // 同上

(v5.4) 如果标题中含有"%s",将替换成原始标题,同时传参到initPage:

WUI.showPage("pageHome", {title: "%s-" + cityName, cityName: cityName}); //e.g. 显示 "首页-上海"

title用于唯一标识tab,即如果相同title的tab存在则直接切换过去。除非:
(v5.5) 如果标题以"!"结尾, 则每次都打开新的tab页,(v7)等价于指定选项showPageOpt.force:1:

WUI.showPage("pageHome", "我的首页!");
WUI.showPage("pageHome", {title: "我的首页", force:1}); // 同上

(v7) 按住Ctrl点击菜单项打开页面, 也会刷新式打开, 相当于force:1选项
按住Shift点击菜单项, 强制打开新页面, 相当于forceNew:1选项;
如果菜单项是链接页面(指定href为一个url), 则会在新窗口打开(默认是在tab页内即pageIframe中打开).

1 showPageOpt.pageFilter: (v6) 指定列表页过滤条件(PAGE_FILTER)

示例

var pageFilter = {cond: {status: "在职"}};
WUI.showPage("pageEmployee", {title: "员工", pageFilter: pageFilter});

它直接影响页面中的datagrid的查询条件。

选项_pageFilterOnly用于影响datagrid查询只用page filter的条件。

var pageFilter = { cond: {status: "在职"}, _pageFilterOnly: true };
WUI.showPage("pageEmployee", {title: "员工", pageFilter: pageFilter});

注意:旧应用中通过paramArr[1]来指定pageFilter, 目前兼容该用法,但不推荐使用:

WUI.showPage("pageEmployee", "员工", [null, pageFilter]); // 旧写法,不推荐使用
// 等价于
WUI.showPage("pageEmployee", {title: "员工", pageFilter: pageFilter});

2 (v6) 返回deferred对象

showPage返回deferred/promise对象,表示页面加载完成。所以如果某些操作要依赖于页面完成,可以用:

var dfd = WUI.showPage(...);
dfd.then(fn);
// fn中的操作需要依赖上面页面。

await WUI.showPage();
fn();

3 showPageOpt.target: 指定显示在哪个tabs中

一般与系统页面pageTab合用,pageTab可显示一个或多个tabs,然后把新页面显示在指定tabs中。示例:

await WUI.showPage("pageTab", {title: "员工!", tabs:"40%,60%"});
WUI.showPage("pageEmployee", {title: "管理员", pageFilter:{cond: {perms:"~mgr"}}, target:"员工_1"});
WUI.showPage("pageEmployee", {title: "非管理员", pageFilter:{cond: {perms:"!~mgr"}}, target:"员工_2"});

注意:下面两个页面target要依赖于pageTab页面,所以需要加await等待pageTab页面加载完成。

@see pageTab

@key wui-pageFile

动态加载的逻辑页(或对话框)具有该属性,值为源文件名。

@fn getPageOpt(jpage)

返回showPage时传入的参数,是一个对象:{title, ...}。
如果jpage不是页面,则返回null。

@fn getPageFilter(jpage, name?)

@key PAGE_FILTER

取页面的过滤参数,由框架自动处理.
返回showPage原始过滤参数或null。注意不要修改它。

如果指定name,则取page filter中的该过滤字段的值。示例:假如page filter有以下4种:

{ cond: { type: "A" }}
{ cond: "type='A'"}
{ cond: {type: "A OR B"} }
{ cond: {type: "~A*"} }

则调用getPageFilter(jpage, "type")时,仅当第1种才返回"A";
其它几种的cond值要么不是对象,要么不是相等条件,均返回null。

@fn saveDlg(jdlg, noCloseDlg=false)

对话框保存,相当于点击确定按钮。如果noCloseDlg=true,则不关闭对话框(即“应用”保存功能)。

@fn closeDlg(jdlg)

@fn dupDlg(jdlg)

复制并添加对象。
对象对话框在更新模式下,按Ctrl-D或右键对话框空白处选择“再次新增”,可复制当前对象,进入添加模式。

支持对子表进行复制,注意仅当子表是允许添加的才会被复制(wui-subobj组件,且设置了valueField和dlg选项)。
(由于子表支持懒加载,当运行“再次新增”时,会自动加载所有子表数据后再复制)

在对话框进入新增模式后,编号(id)会被清除,其它字段保留,
如果对话框编写过新增时的逻辑,比如自动填写或disable某些字段,这些逻辑会生效。

如果字段在新增时是disabled状态,比如“添加时间”等字段,尽管它仍显示为旧数据且无法修改,
但在添加时它并不会提交,所以没有影响。

如果要定制复制时的逻辑,可以使用下面事件:

@key event-duplicate (ev, data) 对话框上复制添加对象

如果要定制添加行为,可在对话框的duplicate事件中修改数据。示例:

jdlg.on("duplicate", function (ev, data) {
    // 可修改data, 若有子表则是有相应的数组字段
    // 主表id,子表id及关联字段在这里可以访问,之后会被框架自动删除,此处无须处理
    console.log(data);
});

@fn isSmallScreen

判断是否为手机小屏显示. 宽度低于640当作小屏处理.

@fn showDlg(jdlg, opt?)

1 对话框加载

2 对话框编程模式

3 对象型对话框与formMode

4 对话框事件

5 reset控制

6 控制底层jquery-easyui对话框

7 复用dialog模板

8 动态生成字段的对话框

@param jdlg 可以是jquery对象,也可以是selector字符串或DOM对象,比如 "#dlgOrder".

注意:当对话框动态从外部加载时,jdlg=$("#dlgOrder") 一开始会为空数组,这时也可以调用该函数,且调用后jdlg会被修改为实际加载的对话框对象。

@param opt ?={url, buttons, noCancel=false, okLabel="确定", cancelLabel="取消", modal=true, reset=true, validate=true, data, onOk, onSubmit, title}

如果是对象对话框(showObjDlg)调用过来,会自动带上以下选项:

1 对话框加载

示例1:静态加载(在web/store.html中的my-pages中定义),通过对话框的id属性标识。

<div id="my-pages" style="display:none">
    <div id="dlgLogin" title="商户登录">  
        ...
    </div>
<div>

加载:WUI.showDlg($("#dlgLogin"))。对话框顶层DOM用div或form都可以。用form更简单些。
除了少数内置对话框,一般不建议这样用,而是建议从外部文件动态加载(模块化)。

示例2:从内部模板动态加载,模板id须为"tpl_{对话框id}",对话框上不用指定id

<script type="text/template" id="tpl_dlgLogin">
    <div title="商户登录">  
        ...
    </div>
</script>

加载:WUI.showDlg($("#dlgLogin"))WUI.showDlg("#dlgLogin")
比示例1要好些,但一般也不建议这样用。目前是webcc编译优化机制使用该技术做发布前常用对话框的合并压缩。

示例3:从外部模板动态加载,模板是个文件如web/page/dlgLogin.html,对话框上不用指定id

<div title="商户登录">  
    ...
</div>

加载:WUI.showDlg($("#dlgLogin"))WUI.showDlg("#dlgLogin")
这是目前使用对话框的主要方式。

示例4:不用HTML,直接JS中创建DOM:

var jdlg = $('<div title="商户登录">Hello World</div>');
WUI.showDlg(jdlg);

适合编程动态实现的对话框。参考使用更简单的WUI.showDlgByMeta或WUI.showDlg的meta选项。

2 对话框编程模式

对话框有两种编程模式,一是通过opt参数在启动对话框时设置属性及回调函数(如onOk),另一种是在dialog初始化函数中处理事件(如validate事件)实现逻辑,有利于独立模块化。

对话框显示时会触发以下事件:

事件beforeshow
事件show
opt.onShow(formMode, data)

对于自动提交数据的对话框(设置了opt.url),提交数据过程中回调函数及事件执行顺序为:

事件validate; // 提交前,用于验证或设置提交数据。返回false或ev.preventDefault()可取消提交,中止以下代码执行。
opt.onSubmit(jdlg, data); // 提交前,验证或设置提交数据,返回false将阻止提交。
... 框架通过callSvr自动提交数据,如添加、更新对象等。
opt.onOk(data); // 提交且服务端返回数据后。data是服务端返回数据。
事件retdata; // 与onOk类似。

对于手动提交数据的对话框(opt.url为空),执行顺序为:

事件validate; // 用于验证、设置提交数据、提交数据。
opt.onOk(); // 同上. 回调函数中this为jdlg.

注意:

调用此函数后,对话框将加上以下CSS Class:

@key .wui-dialog 标识WUI对话框的类名。

示例:显示一个对话框,点击确定后调用后端接口。

WUI.showDlg("#dlgCopyTo", {
    modal: false, 
    reset: false,
    url: WUI.makeUrl("Category.copyTo", {cond: ...}),
    onSubmit: ..., // 提交前验证,返回False则取消提交
    onOk: function (retdata) {
        var jdlgCopyTo = this; // this是当前对话框名
        // 自动提交后处理返回数据retdata
    }
});

如果不使用url选项,也可实现如下:

WUI.showDlg("#dlgCopyTo", {
    modal: false, 
    reset: false,
    onOk: function () {
        var jdlgCopyTo = this; // this是当前对话框名
        var data = WUI.getFormData(jdlgCopyTo);
        callSvr("Category.copyTo", {cond: ...}, function (retdata) { ... }, data);
    }
});

如果要做更多的初始化配置,比如处理对话框事件,则使用初始化函数机制,即在对话框DOM上设置my-initfn属性:

<div title="复制到" my-initfn="initDlgCopyTo"> ... </div>

初始化函数示例:

function initDlgCopyTo()
{
    var jdlg = $(this);

    jdlg.on("beforeshow", onBeforeShow)
        .on("validate", onValidate);

    function onBeforeShow(ev, formMode, opt) {
    }
    function onValidate(ev, mode, oriData, newData) {
    }
}

3 对象型对话框与formMode

函数showObjDlg()会调用本函数显示对话框,称为对象型对话框,用于对象增删改查,它将以下操作集中在一起。
打开窗口时,会设置窗口模式(formMode):

注意:

初始数据与对话框中带name属性的对象相关联,显示对话框时,带name属性的DOM对象将使用数据opt.data自动赋值(对话框show事件中可修改),在点“确定”按钮提交时将改动的数据发到服务端(validate事件中可修改),详见

@see setFormData getFormData

4 对话框事件

操作对话框时会发出以下事件供回调:

create - 对话框创建(在my-initfn执行后)
beforeshow - 对话框显示前。常用来处理对话框显示参数opt或初始数据opt.data.
show - 显示对话框后。常用来设置字段值或样式,隐藏字段、初始化子表datagrid或隐藏子表列等。
validate - 用于提交前验证、补齐数据等。返回false可取消提交。(v5.2) 支持其中有异步操作.
retdata - 服务端返回结果时触发。用来根据服务器返回数据继续处理,如再次调用接口。

注意:

@key event-create (ev)

对话框初始化函数执行后触发。

@key event-beforeshow (ev, formMode, opt)

显示对话框前触发。

注意:每次调用showDlg()都会回调,可能这时对话框已经在显示。

@key event-show (ev, formMode, initData)

对话框显示后事件,用于设置DOM组件。
注意如果在beforeshow事件中设置DOM,对于带name属性的组件会在加载数据时值被覆盖回去,对它们在beforeshow中只能通过设置opt.data来指定缺省值。

@key event-validate (ev, formMode, initData, newData)

initData为初始数据,如果要验证或修改待提交数据,应直接检查form中相应DOM元素的值。如果需要增加待提交字段,可加到newData中去。示例:添加参数: newData.mystatus='CR';

(v5.2) validate事件支持返回Deferred对象支持异步操作.
示例: 在提交前先弹出对话框询问. 由于app_alert是异步对话框, 需要将一个Deferred对象放入ev.dfds数组, 告诉框架等待ev.dfds中的延迟对象都resolve后再继续执行.

jdlg.on("validate", onValidate);
function onValidate(ev, mode, oriData, newData) 
{
    var dfd = $.Deferred();
    app_alert("确认?", "q", function () {
        console.log("OK!");
        dfd.resolve();
    });
    ev.dfds.push(dfd.promise());
}

常用于在validate中异步调用接口(比如上传文件).

@key event-retdata (ev, data, formMode)

form提交后事件,用于处理返回数据

以下事件将废弃:

@key event-initdata (ev, initData, formMode) 加载数据前触发。可修改要加载的数据initData, 用于为字段设置缺省值。将废弃,改用beforeshow事件。

@key event-loaddata (ev, initData, formMode) form加载数据后,一般用于将服务端数据转为界面显示数据。将废弃,改用show事件。

@key event-savedata (ev, formMode, initData) 对于设置了opt.url的窗口,将向后台提交数据,提交前将触发该事件,用于验证或补足数据(修正某个)将界面数据转为提交数据. 返回false或调用ev.preventDefault()可阻止form提交。将废弃,改用validate事件。

@see example-dialog-event 在对话框中使用事件

5 reset控制

对话框上有name属性的组件在显示对话框时会自动清除(除非设置opt.reset=false或组件设置有noReset属性)。

@key .my-reset 标识在显示对话框时应清除

对于没有name属性(不与数据关联)的组件,可加该CSS类标识要求清除。
例如,想在forSet模式下添加显示内容, 而在forFind/forAdd模式下时清除内容这样的需求。

<div class="my-reset">...</div>

@key [noReset]

某些字段希望设置后一直保留,不要被清空,可以设置noReset属性,例如:

<input type="hidden" name="status" value="PA" noReset>

6 控制底层jquery-easyui对话框

wui-dialog可以使用底层easyui-dialog/easyui-window/easyui-panel的选项或方法,参考:

示例:打开时最大化,关闭对话框时回调事件:

var dialogOpt = {  
    maximized: true,
    onClose:function(){
        console.log("close");
    }  
};

jfrm.on("beforeshow",function(ev, formMode, opt) {
    opt.dialogOpt = dialogOpt;
})

(v6) 除了直接指定opt.dialogOpt,还可以直接通过data-options来设置,示例:
不显示折叠、最大化按钮,自定义类名"loginPanel"

<div id="dlgLogin" title="登录" data-options="cls:'loginPanel',collapsible:false,maximizable:false" style="width:350px;height:210px;">  
...
</div>

7 复用dialog模板

(v5.3引入,v6修改) 该机制可用于自定义表(UDT, 对话框动态生成)。

如 dlgUDT_inst_A 与 dlgUDT_inst_B 会共用dlgUDT对话框模板,只要用"inst"分隔对话框模板文件和后缀名。

WUI.showDlg("dlgUDT_inst_A"); // 自动加载page/dlgUDT.html文件

若涉及重用其它模块中的页面或对话框,请参考 WUI.options.moduleExt

8 动态生成字段的对话框

(v6) 该机制可用于为已有对话框动态追加字段(比如用于用户自定义字段UDF),或是只用纯JS而不用html来创建对话框。

示例:为对话框dlgReportCond追加两个输入字段。

var itemArr = [
    // title, dom, hint?
    {title: "状态", dom: '<select name="status" class="my-combobox" data-options="jdEnumMap:OrderStatusMap"></select>'},
    {title: "订单号", dom: "<textarea name='param' rows=5></textarea>", hint: '每行一个订单号'}
];
WUI.showDlg("#dlgReportCond", {
    meta: itemArr,
    onOk: function (data) {
        console.log(data)
    },
    // metaParent: "table:first" // 也可指定插入点父结点
});

通过指定opt.meta动态生成字段,这些字段默认放置于对话框中的第一个table下。
一般详情对话框DOM模型均为"

"。

注意由于对话框有id,只会创建一次。之后再调用也不会再创建。如果希望能创建多的对话框互不影响,可以用"#dlgReportCond_inst_A"这种方式指定是用它的一个实例。

示例2:动态创建一个登录对话框

var jdlg = $('<form title="商户登录"><table></table></form>');
var meta = [
    {title: "用户名", dom: '<input name="uname" class="easyui-validatebox" required>', hint: "字母开头或11位手机号"},
    {title: "密码", dom: '<input type="password" name="pwd" class="easyui-validatebox" required>'}
];
WUI.showDlg(jdlg, {
    meta: meta,
    onOk: function (data) {
        console.log(data); // 根据meta中每个带name项的输入框生成:{uname, pwd}
        callSvr("login", function () {
            app_show("登录成功");
            WUI.closeDlg(jdlg);
        }, data);
    }
});

可以没有title/dom,只用hint用于逻辑说明,示例:

var meta = [
    {hint:'查询<code>工单实际开工日期</code>在指定日期区间的所有受EC到影响的工件'},
    {title: "序列号", dom: '<input name="code">'},
    ...
];
WUI.showDlg("#dlgReportCond", {
    meta: meta,
    onOk: function (data) { ... }
});

@see showDlgByMeta

@see showObjDlg

@fn showDlgByMeta(meta, opt)

WUI.showDlg的简化版本,通过直接指定组件创建对话框。返回动态创建的jdlg。

示例:

var meta = [
    // title, dom, hint?
    {title: "接口名", dom: "<input name='ac' required>", hint: "示例: Ordr.query"},
    {title: "参数", dom: "<textarea name='param' rows=5></textarea>", hint: '示例: {cond: {createTm: ">2020-1-1"}, res: "count(*) cnt", gres: "status"}'}
];
WUI.showDlgByMeta(meta, {
    title: "通用查询",
    modal: false,
    onOk: function (data) {
        app_alert(JSON.stringify(data));
    }
});

@see showDlg 参考opt.meta选项

@fn batchOp(obj, ac, jtbl, opt={data, acName="操作", onBatchDone, batchOpMode=0, queryParam})

1 示例1: 无须对话框填写额外信息的批量操作

2 示例2:打开对话框,批量设置一些信息

3 示例3:打开对话框,先上传文件再批量操作

4 示例4: 基于查询条件的批量操作

基于列表的批量处理逻辑:(v6支持基于查询条件的批量处理逻辑,见下面opt.queryParam)

对表格jtbl中的多选数据进行批量处理,先调用$obj.query(cond)接口查询符合条件的数据条数(cond条件根据jtbl上的过滤条件及当前多选项自动得到),弹出确认框(opt.acName可定制弹出信息),
确认后调用ac(cond)接口对多选数据进行批量处理,处理完成后回调opt.onBatchDone,并刷新jtbl表格。

其行为与框架内置的批量更新、批量删除相同。

@param ac 对象接口名, 如"Task.setIf"/"Task.delIf",也可以是函数接口,如"printSn"

@param opt.acName 操作名称, 如"更新"/"删除"/"打印"等, 一个动词. 用于拼接操作提示语句.

@param opt.data 调用支持批量的接口的POST参数

opt.data也可以是一个函数dataFn(batchCnt),参数batchCnt为当前批量操作的记录数(必定大于0)。
该函数返回data或一个Deferred对象(该对象适时应调用dfd.resolve(data)做批量操作)。dataFn返回false表示不做后续处理。

@return 如果返回false,表示当前非批量操作模式,或参数不正确无法操作。

为支持批量操作,服务端须支持以下接口:

// 对象obj的标准查询接口:
$obj.query($queryParam, res:"count(*) cnt") -> {cnt}
// 批量操作接口ac, 接受过滤查询条件(可通过$obj.query接口查询), 返回实际操作的数量.
$ac($queryParam)($data) -> $cnt

其中obj, ac, data(即POST参数)由本函数参数传入(data也可以是个函数, 返回POST参数), queryParam根据表格jtbl的选择行或过滤条件自动生成.

基于列表的批量操作,完成时会自动刷新表格, 无须手工刷新. 在列表上支持以下批量操作方式:

  1. 基于多选: 按Ctrl/Shift在表上选择多行,然后点操作按钮(如"删除"按钮, 更新时的"确定"按钮),批量操作选中行;生成过滤条件形式是{cond: "id IN (100,101)"},

  2. 基于表格当前过滤条件: 按住Ctrl键(进入批量操作模式)点操作按钮, 批量操作表格过滤条件下的所有行. 若无过滤条件, 自动以{cond: "id>0"}做为过滤条件.

  3. 如果未按Ctrl键, 且当前未选行或是单选行, 函数返回false表示当前非批量处理模式,不予处理。

@param batchOpMode 定制批量操作行为, 比如是否需要按Ctrl激活批量模式, 未按Ctrl时如何处理未选行或单选行。

简单来说, 默认模式对单个记录不处理, 返回false留给调用者处理; 模式2是对单个记录也按批量处理; 模式1是无须按Ctrl键就批量处理.

1 示例1: 无须对话框填写额外信息的批量操作

工件列表页(pageSn)中,点"打印条码"按钮, 打印选中的1行或多行的标签, 如果未选则报错. 如果按住Ctrl点击, 则按表格过滤条件批量打印所有行的标签.

显然, 这是一个batchOpMode=2的操作模式, 调用后端Sn.print接口, 对一行或多行数据统一处理,在列表页pageSn.js中为操作按钮指定操作:

// function initPageSn
var btn1 = {text: "打印条码", iconCls:'icon-ok', handler: function () {
    WUI.batchOp("Sn", "printSn", jtbl, {
        acName: "打印", 
        batchOpMode: 2
    });
}};

jtbl.datagrid({
    ...
    toolbar: WUI.dg_toolbar(jtbl, jdlg, "export", btn1),
});

后端应实现接口printSn(cond), 实现示例:

function api_printSn() {
    // 通过query接口查询操作对象内容. 
    $param = array_merge($_GET, ["res"=>"code", "fmt"=>"array" ]);
    $arr = callSvcInt("Sn.query", $param);
    addLog($rv);
    foreach ($arr as $one) {
        // 处理每个对象
    }
    // 应返回操作数量
    return count($arr);
}

@param opt.queryParam

(v6) 基于查询条件的批量处理,即指定opt.queryParam,这时jtbl参数传null,与表格操作无关,只根据指定条件查询数量和批量操作。
注意jtbl和opt.queryParam必须指定其一。参见下面示例4。

2 示例2:打开对话框,批量设置一些信息

在列表页上添加操作按钮,pageXXX.js:

// 点按钮打开批量上传对话框
var btn1 = {text: "批量设置", iconCls:'icon-add', handler: function () {
    WUI.showDlg("#dlgUpload", {modal:false, jtbl: jtbl}); // 注意:为对话框传入特别参数jtbl即列表的jQuery对象,在batchOp函数中要使用它。
}};
jtbl.datagrid({
    ...
    toolbar: WUI.dg_toolbar(jtbl, jdlg, "export", btn1),
});

对批量设置页面上调用接口,dlgUpload.js:

var jtbl;
jdlg.on("validate", onValidate)
    on("beforeshow", onBeforeShow);

function onBeforeShow(ev, formMode, opt) {
    jtbl = opt.jtbl; // 记录传入的参数
}
function onValidate(ev, mode, oriData, newData) 
{
    WUI.batchOp("Item", "batchSetItemPrice", jtbl, {
        batchOpMode: 1,  // 无须按Ctrl键, 一律当成批量操作
        data: WUI.getFormData(jfrm),
        onBatchDone: function () {
            WUI.closeDlg(jdlg);
        }
    });
}

注意:对主表字段的设置都可在通用的详情对话框上解决(若要批量设置子表,也可通过在set/setIf接口里处理虚拟字段解决)。一般无须编写批量设置操作。

3 示例3:打开对话框,先上传文件再批量操作

在安装任务列表页上,点"批量上传"按钮, 打开上传文件对话框(dlgUpload), 选择上传并点击"确定"按钮后, 先上传文件, 再将返回的附件编号批量更新到行记录上.

先选择操作模式batchOpMode=1, 点确定按钮时总是批量处理.

与示例2不同,上传文件是个异步操作,可为参数data传入一个返回Deferred对象(简称dfd)的函数(onGetData)用于生成POST参数,
以支持异步上传文件操作,在dfd.resolve(data)之后才会执行真正的批量操作.

pageTask.js:

// 点按钮打开批量上传对话框
var btn2 = {text: "批量上传附件", iconCls:'icon-add', handler: function () {
    WUI.showDlg("#dlgUpload", {modal:false, jtbl: jtbl}); // 注意:为对话框传入特别参数jtbl即列表的jQuery对象,在batchOp函数中要使用它。
}};

dlgUpload.js:

var jtbl;
jdlg.on("validate", onValidate)
    on("beforeshow", onBeforeShow);

function onBeforeShow(ev, formMode, opt) {
    jtbl = opt.jtbl; // 记录传入的参数
}
function onValidate(ev, mode, oriData, newData) 
{
    WUI.batchOp("Task", "Task.setIf", jtbl, {
        batchOpMode: 1,  // 无须按Ctrl键, 一律当成批量操作
        data: onGetData,
        onBatchDone: function () {
            WUI.closeDlg(jdlg);
        }
    });
}

// 一定batchCnt>0. 若batchCnt=0即没有操作数据时, 会报错结束, 不会回调该函数.
function onGetData(batchCnt)
{
    var dfd = $.Deferred();
    app_alert("批量上传附件到" + batchCnt + "行记录?", "q", function () {
        var dfd1 = triggerAsync(jdlg.find(".wui-upload"), "submit"); // 异步上传文件,返回Deferred对象
        dfd1.then(function () {
            var data = WUI.getFormData(jfrm);
            if ($.isEmptyObject(data)) {
                app_alert("没有需要更新的内容。");
                return false;
            }
            dfd.resolve(data);
        });
    });
    return dfd.promise();
}

@see triggerAsync 异步事件调用

上面函数中处理异步调用链,不易理解,可以简单理解为:

if (confirm("确认操作?") == no)
    return;
jupload.submit();
return getFormData(jfrm);

4 示例4: 基于查询条件的批量操作

示例:在工单列表页,批量为工单中的所有工件打印跟踪码。

工单为Ordr对象,工件为Sn对象。注意:此时是操作Sn对象,而非当前Ordr对象,所以不传jtbl,而是直接传入查询条件.

WUI.batchOp("Sn", "printSn", null, {
    acName: "打印", 
    queryParam: {cond: "orderId=80"},
    data: {tplId: 10}
});

后端批量打印接口设计为:

printSn(cond, tplId) -> cnt

上例中,查询操作数量时将调用接口callSvr("Sn.query", {cond: "orderId=80", res: "count(*) cnt"})
在批量操作时调用接口callSvr("printSn", {cond: "orderId=80"}, $.noop, {tplId: 10})

@fn getTopDialog()

取处于最上层的对话框。如果没有,返回jo.size() == 0

@fn unloadPage(pageName?)

@param pageName 如未指定,表示当前页。

删除一个页面。一般用于开发过程,在修改外部逻辑页后,调用该函数删除页面。此后载入页面,可以看到更新的内容。

注意:对于内部逻辑页无意义。

@var g_data.doUpload

(v7) 为便于在reload页面时定制逻辑,在unload页面时会设置g_data.doUnload变量为true,可以页面pagedestory事件中检测处理,如:

jpage.on("pagedestroy", onPageDestroy);
function onPageDestroy() {
    if (g_data.doUnload) {
        console.log('!!! unload page');
    }
}

meta页面(或二次开发的页面)支持在更新meta后,点“刷新页面”重新加载meta,就是使用此机制。

@fn reloadPage()

重新加载当前页面。一般用于开发过程,在修改外部逻辑页后,调用该函数可刷新页面。

@fn unloadDialog(jdlg?)

@alias reloadDialog

删除指定对话框jdlg,如果不指定jdlg,则删除当前激活的对话框。一般用于开发过程,在修改外部对话框后,调用该函数清除以便此后再载入页面,可以看到更新的内容。

WUI.reloadDialog(jdlg);
WUI.reloadDialog();
WUI.reloadDialog(true); // 重置所有外部加载的对话框(v5.1)

注意:

@fn canDo(topic, cmd=null, defaultVal=null, permSet2=null)

1 关于页面与对话框

权限检查回调,支持以下场景:

  1. 页面上的操作(按钮)

    canDo(页面标题, 按钮标题);// 返回false则不显示该按钮

  2. 对话框上的操作

    canDo(对话框标题, "对话框"); // 返回false表示对话框只读
    canDo(对话框标题, 按钮标题); // 返回false则不显示该按钮

特别地:如果对话框或页面上有wui-readonly类,则额外优先用permSet2来检查:

canDo(对话框, 按钮标题, null, {只读:true}); // 返回false则不显示该按钮

topic可理解为数据对象(页面、对话框对应的数据模型),cmd可理解为操作(增加、修改、删除、只读等,常常是工具栏按钮)。
通过permSet2参数可指定额外权限。

判断逻辑示例:canDo("工艺", "修改")

如果指定有"工艺.修改",则返回true,或指定有"工艺.不可修改",则返回false;否则
如果指定有"修改",则返回true,或指定有"不可修改", 则返回 false; 否则
如果指定有"工艺.只读" 或 "只读",则返回false; 否则
如果指定有"工艺",则返回true,或指定有"不可工艺", 则返回 false; 否则返回默认值。

判断逻辑示例:canDo("工艺", null)

如果指定有"工艺",则返回true,或指定有"不可工艺", 则返回 false; 否则默认值

判断逻辑示例:canDo(null, "修改")

如果指定有"修改",则返回true,或指定有"不可修改", 则返回 false; 否则
如果指定有"只读",则返回false; 否则返回默认值。

默认值逻辑:

如果指定了默认值defaultVal,则返回defaultVal,否则
如果指定有"不可*",则默认值为false,否则返回 true
(注意:如果未指定"*"或"不可*",最终是允许)

特别地,对于菜单显示来说,顶级菜单的默认值指定是false,所以如果未指定""或"不可"则最终不显示;
而子菜单的默认值则是父菜单是否允许,不指定则默认与父菜单相同。

建议明确指定默认值,采用以下两种风格之一:

风格一:默认允许,再逐一排除

* 不可删除 不可导出 不可修改

风格二:默认限制,再逐一允许

不可* 工单管理

要限制菜单项的话,先指定"不可*",再加允许的菜单项,这样如果页面中链接其它页面或对话框,则默认是无权限的。
否则链接对象默认是可编辑的,存在漏洞。

TODO:通过设置 WUI.options.canDo(topic, cmd) 可扩展定制权限。

默认情况下,所有菜单不显示,其它操作均允许。
如果指定了"*"权限,则显示所有菜单。
如果指定了"不可XX"权限,则topic或cmd匹配XX则不允许。

@key wui-perm

示例:假设有菜单结构如下(不包含最高管理员专有的“系统设置”)

主数据管理
    企业
    用户

运营管理
    活动
    公告
    积分商城

只显示“公告”菜单项:

公告

只显示“运营管理”菜单组:

运营管理

显示除了“运营管理”外的所有内容:

* 不可运营管理

其中*表示显示所有菜单项。
显示所有内容(与管理员权限相同),但只能查看不可操作

* 只读

“只读”权限排除了“新增”、“修改”等操作。
特别地,“只读”权限也不允许“导出”操作(虽然导出是读操作,但一般要求较高权限),假如要允许导出公告,可以设置:

* 只读 公告.导出

显示“运营管理”,在列表页中不显示“删除”、“导出”按钮:

运营管理 不可删除 不可导出

显示“运营管理”,在列表页中,不显示“删除”、“导出”按钮,但“公告”中显示“删除”按钮:

运营管理 不可删除 不可导出 公告.删除

或等价于:

运营管理 不可导出 活动.不可删除 积分商城.不可删除

显示“运营管理”和“主数据管理”菜单组,但“主数据管理”下面内容均是只读的:

运营管理 主数据管理 企业.只读 用户.只读

1 关于页面与对话框

假如在“活动”页面中链接了“用户”对话框(或“活动”页面上某操作按钮会打开“用户”页面),即使该角色只有“活动”权限而没有“用户”的权限,也能正常打开用户对话框或页面并修改用户。
这是一个潜在安全漏洞,在配置权限时应特别注意。

这样设计是因为用户权限主要是针对菜单项的,而且可以只指定到父级菜单(表示下面子菜单均可显示);这样就导致对未指定的权限,也无法判断是否可用(因为可能是菜单子项),目前处理为默认可用(可通过权限不可*来指定默认不可用)。

以指定运营管理下所有功能为例,解决办法:

@fn doFind(jo, jtbl?, doAppendFilter?=false)

根据对话框中jo部分内容查询,结果显示到表格(jtbl)中。
jo一般为对话框内的form或td,也可以为dialog自身。
查询时,取jo内部带name属性的字段作为查询条件。如果有多个字段,则生成AND条件。

如果查询条件为空,则不做查询;但如果指定jtbl的话,则强制查询。

jtbl未指定时,自动取对话框关联的表格;如果未关联,则不做查询。
doAppendFilter=true时,表示追加过滤条件。

@see .wui-notCond 指定独立查询条件

@fn showObjDlg(jdlg, mode, opt?={jtbl, id, obj})

@param jdlg 可以是jquery对象,也可以是selector字符串或DOM对象,比如 "#dlgOrder". 注意:当对话框保存为单独模块时,jdlg=$("#dlgOrder") 一开始会为空数组,这时也可以调用该函数,且调用后jdlg会被修改为实际加载的对话框对象。

@param opt.id String. 对话框set模式(mode=FormMode.forSet)时可设置,表示从后端加载数据,set/del如缺省则从关联的opt.jtbl中取, add/find时不需要

@param opt.jtbl Datagrid. 指定对话框关联的列表(datagrid),用于从列表中取值,或最终自动刷新列表。 -- 如果dlg对应多个tbl, 必须每次打开都设置

@param opt.obj String. (v5.1) 对象对话框的对象名,如果未指定,则从my-obj属性获取。通过该参数可动态指定对象名。

@param opt.offline Boolean. (v5.1) 不与后台交互。

@param opt.readonly String. (v5.5) 指定对话框只读。即设置wui-readonly类。

showObjDlg底层通过showDlg实现,(v5.5)showObjDlg的opt会合并到showDlg的opt参数中,同时showDlg的opt.objParam将保留showObjDlg的原始opt。在每次打开对话框时,可以从beforeshow回调事件参数中以opt.objParam方式取出.
以下代码帮助你理解这几个参数的关系:

function showObjDlg(jdlg, mode, opt)
{
    opt = $.extend({}, jdlg.objParam, opt);
    var showDlgOpt = $.extend({}, opt, {
        ...
        objParam: opt
    });
    showDlg(jdlg, showDlgOpt);
}
jdlg.on("beforeshow", function (ev, formMode, opt) {
    // opt即是上述showDlgOpt
    // opt.objParam为showObjDlg的原始opt,或由jdlg.objParam传入
});

@param opt.title String. (v5.1) 指定对话框标题。

@param opt.data Object. (v5.5) 为对话框指定初始数据,对话框中name属性匹配的控件会在beforeshow事件后且show事件前自动被赋值。

param opt.onOk Function(retData) (v6) 与showDlg的onOk参数一致。在提交数据后回调,参数为后端返回数据,比如add接口返回新对象的id。

注意:如果是forSet模式的对话框,即更新数据时,只有与原始数据不同的字段才会提交后端。

其它参数可参考showDlg函数的opt参数。

@key objParam 对象对话框的初始参数。

(v5.1)
此外,通过设置jdlg.objParam,具有和设置opt参数一样的功能,常在initPageXXX中使用,因为在page中不直接调用showObjDlg,无法直接传参数opt.
示例:

var jdlg = $("#dlgSupplier");
jdlg.objParam = {type: "C", obj: "Customer"};
showObjDlg(jdlg, FormMode.forSet, {id:101});
// 等价于 showObjDlg(jdlg, FormMode.forSet, {id:101, obj: "Customer", type: "C"});

在dialog的事件beforeshow(ev, formMode, opt)中,可以通过opt.objParam取出showObjDlg传入的所有参数opt。
(v5.3) 可在对象对话框的初始化函数中使用 initDlgXXX(opt),注意:非对象对话框初始化函数的opt参数与此不同。

@param opt.onCrud Function(). (v5.1) 对话框操作完成时回调。

一般用于点击表格上的增删改查工具按钮完成操作时插入逻辑。
在回调函数中this对象就是objParam,可通过this.mode获取操作类型。示例:

jdlg1.objParam = {
    offline: true,
    onCrud: function () {
        if (this.mode == FormMode.forDel) {
            // after delete row
        }

        // ... 重新计算金额
        var rows = jtbl.datagrid("getData").rows, amount = 0;
        $.each(rows, function(e) {
            amount += e.price * e.qty;
        })
        frm.amount.value = amount.toFixed(2);
        // ... 刷新关联的表格行
        // opt.objParam.reloadRow();
    }
};
jtbl.datagrid({
    toolbar: WUI.dg_toolbar(jtbl, jdlg1), // 添加增删改查工具按钮,点击则调用showObjDlg,这时objParam生效。
    onDblClickRow: WUI.dg_dblclick(jtbl, jdlg1),
    ...
});

在dialog逻辑中使用objParam:

function initDlgXXX() {
    // ...
    jdlg.on("beforeshow", onBeforeShow);

    function onBeforeShow(ev, formMode, opt) {
        var objParam = opt.objParam; // {id, mode, jtbl?, offline?...}
    }
}

(v7) 通过jdlg.prop('objParam')可取到对象对话框的参数。

@param opt.reloadRow () 可用于刷新本对话框关联的表格行数据

事件参考:

@see showDlg

@fn dg_toolbar(jtbl, jdlg, button_lists...)

@param jdlg 可以是对话框的jquery对象,或selector如"#dlgOrder".

设置easyui-datagrid上toolbar上的按钮。缺省支持的按钮有r(refresh), f(find), a(add), s(set), d(del), 可通过以下设置方式修改:

// jtbl.jdata().toolbar 缺省值为 "rfasd"
jtbl.jdata().toolbar = "rfs"; // 没有a-添加,d-删除.
// (v5.5) toolbar也可以是数组, 如 ["r", "f", "s", "export"]; 空串或空数组表示没有按钮.

如果要添加自定义按钮,可通过button_lists一一传递.
示例:添加两个自定义按钮查询“今天订单”和“所有未完成订单”。

function getTodayOrders()
{
    var queryParams = WUI.getQueryParam({comeTm: new Date().format("D")});
    WUI.reload(jtbl, null, queryParams);
}
// 显示待服务/正在服务订单
function getTodoOrders()
{
    var queryParams = {cond: "status=" + OrderStatus.Paid + " or status=" + OrderStatus.Started};
    WUI.reload(jtbl, null, queryParams);
}
var btn1 = {text: "今天订单", iconCls:'icon-search', handler: getTodayOrders};
var btn2 = {text: "所有未完成", iconCls:'icon-search', handler: getTodoOrders};

// 默认显示当天订单
var queryParams = WUI.getQueryParam({comeTm: new Date().format("D")});

var dgOpt = {
    url: WUI.makeUrl(["Ordr", "query"]),
    queryParams: queryParams,
    pageList: ...
    pageSize: ...
    // "-" 表示按钮之间加分隔符
    toolbar: WUI.dg_toolbar(jtbl, jdlg, btn1, "-", btn2),
    onDblClickRow: WUI.dg_dblclick(jtbl, jdlg)
};
jtbl.datagrid(dgOpt);

特别地,要添加导出数据到Excel文件的功能按钮,可以增加参数"export"作为按钮定义:
导入可以用"import", 快速查询可以用"qsearch" (这两个以扩展方式在jdcloud-wui-ext.js中定义),复制可以用"dup":

var dgOpt = {
    ...
    toolbar: WUI.dg_toolbar(jtbl, jdlg, "import", "export", "dup", "-", btn1, btn2, "qsearch"),
}

@var toolbar-dup 复制

(v7) 复制一行或多行数据,调用后端{Obj}.dup(id)接口。

@see toolbar-qsearch 模糊查询

如果想自行定义导出行为参数,可以参考WUI.getExportHandler

@see getExportHandler 导出按钮设置

按钮的权限(即是否显示)取决于wui-perm和text属性。优先使用wui-perm。系统内置的常用的有:"新增", "修改", "删除", "导出"
下面例子,把“导入”特别设置为内置的权限“新增”,这样不仅不必在角色管理中设置,且设置了“只读”等权限也可自动隐藏它。

var btnImport = {text: "导入", "wui-perm": "新增", iconCls:'icon-ok', handler: function () {
    DlgImport.show({obj: "Ordr"}, function () {
        WUI.reload(jtbl);
    });
}};

支持定义扩展,比如importOrdr:

// ctx = {jtbl, jp, jdlg} // jp是jpage或jdlg,为上一层容器。jdlg是表格关联的对话框,
// 注意jdlg在调用时可能尚未初始化,可以访问 jdlg.selector和jdlg.objParam等。
dg_toolbar.importOrdr = function (ctx) {
    return {text: "导入", "wui-perm": "新增", iconCls:'icon-ok', handler: function () {
        DlgImport.show({obj: "Ordr"}, function () {
            WUI.reload(jtbl);
        });
    }}
};

这时就可以直接这样来指定导入按钮(便于全局重用):

WUI.dg_toolbar(jtbl, jdlg, ..., "importOrdr")

@key event-dg_toolbar (ev, jtbl, jdlg) 定制列表按钮事件

示例:为订单列表增加一个“关联商品”按钮

$(document).on("dg_toolbar", ".wui-page.pageOrder", pageOrder_onToolbar);
// 用于二次开发,更成熟的写法像这样: UiMeta.on("dg_toolbar", "售后工单", function (ev, buttons, jtbl, jdlg) { ... })
function pageOrder_onToolbar(ev, buttons, jtbl, jdlg) {
    // var jpage = $(ev.target);
    // console.log(jpage);
    var btnLinkToItem = {text: "关联商品", iconCls: "icon-redo", handler: function () {
        var row = WUI.getRow(jtbl);
        if (row == null)
            return;
        var pageFilter = { cond: {id: row.itemId} };
        PageUi.show("商品", "关联商品-订单"+row.id, pageFilter);
    }};
    buttons.push(btnLinkToItem);
}

@key example-setToolbarMenu 动态修改数据表上的工具栏菜单状态

在dg_toolbar事件中,如果想指定datagrid选项,比如要实现需求:
单击一行时,自动根据当前行状态,设置菜单项是否disabled(比如非CR状态时,“删除”菜单不可点击):

jtbl.prop("datagridOpt", {
    onClickRow: function (rowIndex, rowData) {
        var val = (rowData.status == 'CR');
        WUI.setToolbarMenu(jtbl, {删除: val});
        // 因为框架可能扩展过datagrid一些功能,加上这句比较稳妥
        $.fn.datagrid.defaults.onClickRow(rowIndex, rowData);
    }
});

@see setToolbarMenu

@key datagridOpt 二次开发中datagrid选项设置

@fn setToolbarMenu(jtbl, enableMap)

设置数据表工具栏上的菜单项,禁用或启用:

WUI.setToolbarMenu(jtbl, {删除: false, 导出: true}); // 菜单项“删除”(即菜单标题)显示为禁用,“导出”显示为可用。

也可以通过回调函数(或lambda)操作该菜单项:

WUI.setToolbarMenu(jtbl, {
    删除: ji => ji.linkbutton("disable"), // 禁用,等同于设置false
    导出: ji => ji.linkbutton("enable"), // 启用,等同于设置true
    // 导入: ji => ji.toggle(true)
    导入: function (ji) {
        var show = ...;
        ji.toggle(Show); // 显示或隐藏
    }
});

@fn dg_dblclick(jtbl, jdlg)

@param jdlg 可以是对话框的jquery对象,或selector如"#dlgOrder".

设置双击datagrid行的回调,功能是打开相应的dialog

@key a[href=#page]

页面中的a[href]字段会被框架特殊处理:

<a href="#pageHome">首页</a>
<a href="http://baidu.com">百度</a>

@fn getExportHandler(jtbl, ac?, param?={})

为数据表添加导出Excel菜单,如:

jtbl.datagrid({
    url: WUI.makeUrl("User.query"),
    toolbar: WUI.dg_toolbar(jtbl, jdlg, {text:'导出', iconCls:'icon-save', handler: WUI.getExportHandler(jtbl) }),
    onDblClickRow: WUI.dg_dblclick(jtbl, jdlg)
});

默认是导出数据表中直接来自于服务端的字段,并应用表上的查询条件及排序。
也可以通过设置param参数手工指定,如:

handler: WUI.getExportHandler(jtbl, "User.query", {res: "id 编号, name 姓名, createTm 注册时间", orderby: "createTm DESC"})

注意:由于分页机制影响,会设置参数{pagesz: -1}以便在一页中返回所有数据,而实际一页能导出的最大数据条数取决于后端设置(默认1000,参考后端文档 AccessControl::$maxPageSz)。

会根据datagrid当前设置,自动为query接口添加res(输出字段), cond(查询条件), fname(导出文件名), orderby(排序条件)参数。

若是已有url,希望从datagrid获取cond, fname等参数,而不要覆盖res参数,可以这样做:

var url = WUI.makeUrl("PdiRecord.query", ...); // makeUrl生成的url具有params属性,为原始查询参数
var btnExport = {text:'导出', iconCls:'icon-save', handler: WUI.getExportHandler(jtbl, null, {res: url.params.res || null}) };

@see getQueryParamFromTable 获取datagrid的当前查询参数

@fn getQueryParamFromTable(jtbl, param?)

@alias getParamFromTable

根据数据表当前设置,获取查询参数。
可能会设置{cond, orderby, res, fname}参数。但如果param中明确指定了比如res参数,则不会覆盖该参数。

res参数从列设置中获取,如"id 编号,name 姓名", 特别地,如果列对应字段以"_"结尾,不会加入res参数。

(v5.2)
如果表上有多选行,则导出条件为cond="t0.id IN (id1, id2)"这种形式。

fname自动根据当前页面的title以及datagrid当前的queryParam自动拼接生成。
如title是"检测记录报表", queryParam为"tm>='2020-1-1' and tm<='2020-7-1",则生成文件名fname="检测记录报表-2020-1-1-2020-7-1".

@see getExportHandler 导出Excel

@fn getDgInfo(jtbl, res?) -> { opt, isTreegrid, datagrid, url, param, ac, obj, sel?, selArr?, res?, dgFilter? }

取datagrid关联信息. 返回字段标记?的须显式指定,如:

var dg = WUI.getDgInfo(jtbl); // {opt, url, ...}
var dg = WUI.getDgInfo(jtbl, {res: null}); // 多返回res字段
var data = jtbl[dg.datagrid]("getData"); // 相当于jtbl.datagrid(...), 但兼容treegrid调用。

@fn Formatter.atts

列表中显示附件(支持多个), 每个附件一个链接,点击后可下载该附件。(使用服务端att接口)

@fn Formatter.picx(opt={thumb?, preview?})

显示图片(支持多图), 每个图可以有预览, 点击后在新页面打开并依次显示所有的图片.(使用服务端pic接口)

@alias Formatter.pics

相当于picx({thumb:1, preview:3})。显示图片列表预览,点击链接显示图片列表。

@alias Formatter.pics1

相当于picx({thumb:1})。不显示图片列表预览,点击链接显示图片列表。

@fn Formatter.flag(yes, no)

显示flag类的值,示例:

<th data-options="field:'clearFlag', sortable:true, formatter:Formatter.flag("已结算", "未结算"), styler:Formatter.enumStyler({1:'Disabled',0:'Warning'}, 'Warning')">结算状态</th>

注意flag字段建议用Formatter.enum和jdEnumMap,因为在导出表格时,只用flag的话,导出值还是0,1无法被转换,还不如定义一个Map来的更清晰。

@see datagrid.formatter

@see Formatter.enum

@fn Formatter.enum(enumMap, sep=',')

将字段的枚举值显示为描述信息。示例:

    <th data-options="field:'status', jdEnumMap: OrderStatusMap, formatter: WUI.formatter.enum(OrderStatusMap)">状态</th>

如果状态值为"CR",则显示为"未付款". 全局变量OrderStatusMap在代码中定义如下(一般在web/app.js中定义)

var OrderStatusMap = {
    CR: "未付款", 
    PA: "待服务"
}

常用的YesNoMap是预定义的0-否,1-是映射,示例:

<th data-options="field:'clearFlag', sortable:true, jdEnumMap:YesNoMap, formatter:Formatter.enum(YesNoMap), styler:Formatter.enumStyler({1:'Disabled',0:'Warning'}, 'Warning')">已结算</th>

特别地,可以为null指定值:

<th data-options="field:'name', sortable:true, formatter:Formatter.enum({null:'(默认)'})">页面名</th>

@see datagrid.formatter

@see Formatter.enumStyler

@fn Formatter.enumStyler(colorMap, defaultColor?, field?)

为列表的单元格上色,示例:

<th data-options="field:'status', jdEnumMap: OrderStatusMap, formatter:Formatter.enum(OrderStatusMap), styler:Formatter.enumStyler({PA:'Warning', RE:'Disabled', CR:'#00ff00', null: 'Error'}), sortable:true">状态</th>

颜色可以直接用rgb表示如'#00ff00',或是颜色名如'red'等,最常用是用系统预定的几个常量'Warning'(黄), 'Error'(红), 'Info'(绿), 'Disabled'(灰).
缺省值可通过defaultColor传入。

如果是根据其它字段来判断,使用field选项指定字段,示例: 显示statusStr字段,但根据status字段值来显示颜色(默认'Info'颜色)

<th data-options="field:'statusStr', styler:Formatter.enumStyler({PA:'Warning'}, 'Info', 'status'), sortable:true">状态</th>

@see datagrid.styler

@see Formatter.enumFnStyler 比enumStyler更强大

@fn Formatter.enumFnStyler(colorMap, defaultColor)

为列表的单元格上色,示例:

<th data-options="field:'id', sortable:true, sorter:intSort, styler:Formatter.enumFnStyler({'v<10': 'Error', 'v>=10&&v<20': 'Warning'}, 'Info')">编号</th>

每个键是个表达式(其实是个函数),特殊变量v和row分别表示当前列值和当前行。缺省值可通过defaultColor传入。

@see Formatter.enumStyler

@fn Formatter.progress

以进度条方式显示百分比,传入数值value为[0,1]间小数:

<th data-options="field:'progress', formatter: Formatter.progress">工单进度</th>

如果要定制颜色等样式,可以加个styler,如

<th data-options="field:'progress', formatter: Formatter.progress, styler: OrdrFormatter.progressStyler">工单进度</th>

通过为cell指定一个class来控制内部进度条样式:

progressStyler: function (value, row) {
    var st = ... // 'info', 'error'
    return {class: st}; // 直接返回字符串表示styler; 也可以返回 {class, style}这样
}

然后通过CSS类来改写进度条颜色:

<style>
.info .progressbar-value {
    background-color: rgb(190, 247, 190) !important;
}
.error .progressbar-value {
    background-color: rgb(253, 168, 172) !important;
}
</style>

@var formatter = {dt, number, pics, flag(yes?=是,no?=否), enum(enumMap), linkTo(field, dlgRef, showId?=false) }

常常应用定义Formatter变量来扩展WUI.formatter,如

var Formatter = {
    userId: WUI.formatter.linkTo("userId", "#dlgUser"), // 显示用户名(value),点击后打开用户明细框
    storeId: WUI.formatter.linkTo("storeId", "#dlgStore", true), // 显示"商户id-商户名", 点击后打开商户明细框
    orderStatus: WUI.formatter.enum({CR: "新创建", CA: "已取消"}) // 将CR,CA这样的值转换为显示文字。
};
Formatter = $.extend(WUI.formatter, Formatter);

可用值:

在datagrid中使用:

<th data-options="field:'createTm', sortable:true, formatter:Formatter.dt">创建时间</th>
<th data-options="field:'amount', sortable:true, sorter: numberSort, formatter:Formatter.number">金额</th>
<th data-options="field:'userName', sortable:true, formatter:Formatter.linkTo('userId', '#dlgUser')">用户</th>
<th data-options="field:'status', sortable:true, jdEnumMap: OrderStatusMap, formatter: Formatter.orderStatus">状态</th>
<th data-options="field:'done', sortable:true, formatter: Formatter.flag()">已处理</th>

@key datagrid.sumFields 数据表统计列

为datagrid扩展属性,用于在数据表底部显示统计值。
默认统计逻辑是当前页面内累加,如:

var dgOpt = {
    url: WUI.makeUrl("Contract.query"),
    showFooter: true, // 指定要显示底部统计
    sumFields: ["amount", "invoiceAmount", "recvAmount"], // 指定哪些列加统计值
    ...
});
jtbl.datagrid(dgOpt);

如果想跨页统计,即显示所有页数据的统计(在当前查询条件下),需要为query调用添加statRes参数,如:

var dgOpt = {
    url: WUI.makeUrl("Contract.query", {
        // 指定返回amount, invoiceAmount两个统计列
        statRes: "SUM(amount) amount, SUM(invoiceAmount) invoiceAmount",
    }),
    showFooter: true, // 指定要显示底部统计
    sumFields: ["amount", "invoiceAmount", "recvAmount"], // 注意:此时amount,invoiceAmount由于在statRes中指定,是跨页数据统计,而recvAmount未在statRes中指定,则只统计当前显示页。
    ...
});
jtbl.datagrid(dgOpt);

@key datagrid.quickAutoSize

扩展属性quickAutoSize。当行很多且列很多时,表格加载极慢。如果是简单表格(全部列都显示且自动大小,没有多行表头等),可以用这个属性优化。
在pageSimple中默认quickAutoSize为true。

var dgOpt = {
    ...
    pageSize: 200, // 默认一页20,改为200后,默认性能将显著下降; 设置为500后,显示将超过10秒
    pageList: [200, 500, 1000],
    quickAutoSize: true // WUI对easyui-datagrid的扩展属性,用于大量列时提升性能. 参考: jquery.easyui.min.js
};
jtbl.datagrid(dgOpt);

其原因是easyui-datagrid的autoSizeColumn方法有性能问题。当一页行数很多时可尝试使用quickAutoSize选项。

@key .noData

CSS类, 可定义无数据提示的样式

@var GridHeaderMenu

表头左上角右键菜单功能。

扩展示例:

WUI.GridHeaderMenu.items.push('<div id="showObjLog">操作日志</div>');
WUI.GridHeaderMenu.showObjLog = function (jtbl) {
    var row = WUI.getRow(jtbl);
    if (!row)
        return;
    ...
    WUI.showPage("pageObjLog", "操作日志!", [null, param]);
};

@fn showDlgQuery(data?={ac, param})

显示高级查询对话框。在表头左上角右键菜单中有“高级查询”对话框。

@fn checkIdCard(idcard)

身份证校验,示例:

var rv = WUI.checkIdCard("310115200809090927"); // false
var rv = WUI.checkIdCard("310115200809090928"); // true

@key .easyui-validatebox

为form中的组件加上该类,可以限制输入类型,如:

<input name="amount" class="easyui-validatebox" data-options="validType:'number'" required>

validType还支持:

注意:

其它自定义规则(或改写上面规则),可通过下列方式扩展:

$.extend($.fn.validatebox.defaults.rules, {
    workday: {
        validator: function(value) {
            return value.match(/^[1-7,abc]+$/);
        },
        message: '格式例:"1,3a,5b"表示周一,周三上午,周五下午.'
    }
});

@key easyui-linkbutton

@key EXT_LINK_BUTTON

datagrid options中的toolbar,我们使用代码指定方式,即

var btnFind = {text:'查询', iconCls:'icon-search', handler: function () {
    showObjDlg(ctx.jdlg, FormMode.forFind, {jtbl: ctx.jtbl});
};
jtbl.datagrid({ ... 
    toolbar: WUI.dg_toolbar(jtbl, jdlg, ... btnFind)
});

缺点是它只能使用默认的linkbutton组件(在easyui里写死了)。
此处进行hack,增加class属性,让它支持splitbutton/menubutton,示例:

var jmneu = $('#mm').menu({
    onClick: function (item) {
        console.log(item.id);
    }
});
var btnFind = {text:'查询', class: 'splitbutton', iconCls:'icon-search', handler: ..., menu: jmenu};

@key .wui-form-table

在wui-dialog上,对于form下直接放置的table,一般用于字段列表排列,框架对它添加类wui-form-table并自动对列设置百分比宽度,以自适应显示。

在非对话框上,也可手工添加此类来应用该功能。

@var isBusy

标识应用当前是否正在与服务端交互。一般用于自动化测试。

@var g_args 全局URL参数

应用参数。

URL参数会自动加入该对象,例如URL为 http://{server}/{app}/index.html?orderId=10&dscr=上门洗车,则该对象有以下值:

g_args.orderId=10; // 注意:如果参数是个数值,则自动转为数值类型,不再是字符串。
g_args.dscr="上门洗车"; // 对字符串会自动进行URL解码。

框架会自动处理一些参数:

@see parseQuery URL参数通过该函数获取。

@var g_data = {userInfo?, initClient?, hasRole()}

应用全局共享数据。

在登录时,会自动设置userInfo属性为个人信息。所以可以通过 g_data.userInfo==null 来判断是否已登录。

@var BASE_URL

TODO: remove

设置应用的基本路径, 应以"/"结尾.

@var WUI.options

{appName=user, title="客户端", onShowLogin, pageHome="pageHome", pageFolder="page"}

@var WUI.options.moduleExt

用于模块扩展。有两个回调函数选项:

// 定制模块的页面路径
WUI.options.moduleExt.showPage = function (name) {
    // name为showPage或showDlg函数调用时的页面/对话框;返回实际页面地址;示例:
    var map = {
        "pageOrdr__Mes.html": "page/mes/pageOrdr.html",
        "pageOrdr__Mes.js": "page/mes/pageOrdr.js",
    };
    return map[name] || name;
}
// 定制模块的接口调用地址
WUI.options.moduleExt.callSvr = function (name) {
    // name为callSvr调用的接口名,返回实际URL地址;示例:
    var map = {
        "Ordr__Mes.query" => "../../mes/api/Ordr.query",
        "Ordr__Item.query" => "../../mes/api/Item.query"
    }
    return map[name] || name;
}

详细用法案例,可参考:筋斗云开发实例讲解 - 系统复用与微服务方案。

@var WUI.options.xparam

@var MUI.options.useNewThumb

带缩略图的图片编号保存风格。

@var LANG 多国语言支持/翻译

系统支持通过URL参数lang指定语言,如指定英文版本:http://myserver/myapp/web/store.html?lang=en

如果未指定lang参数,则根据html的lang属性来确定语言,如指定英文版:

<html lang="en">

默认为开发语言(lang="dev"),以中文为主。英文版下若想切换到开发语言,可以用http://myserver/myapp/web/store.html?lang=dev
g_args.lang中保存着实际使用的语言。

自带英文语言翻译文件lib/lang-en.js,当lang=en时加载它。可扩展它或以它为模板创建其它语言翻译文件。
语言翻译文件中,设置全局变量LANG,将开发语言翻译为其它语言。

系统会自动为菜单项、页面标题、列表表头标题、对话框标题等查找翻译。
其它DOM组件若想支持翻译,可手工添加CSS类lang,如:

<div><label class="lang"><input type="checkbox" value="mgr">最高管理员</label></div>

<a href="javascript:;" onclick="logout()" class="logout"><span class="lang"><i class="icon-exit"></i>退出系统</span></a>

或在代码中,使用WUI.enhanceLang(jo)来为DOM组件支持翻译,或直接用T(str)翻译字符串。
注意lang类或enhanceLang函数不能设置组件下子组件的文字,可先取到文字组件再设置如WUI.enhanceLang(jo.find(".title"))

@fn T (s, defVal?) 字符串翻译

T函数用于将开发语言翻译为当前使用的语言。

@key .lang DOM组件支持翻译

@fn enhanceLang (jo) DOM组件支持翻译

@fn app_alert(msg, [type?=i], [fn?], opt?={timeoutInterval?, defValue?, onCancel()?})

@param type 对话框类型: "i": info, 信息提示框; "e": error, 错误框; "w": warning, 警告框; "q"(与app_confirm一样): question, 确认框(会有"确定"和"取消"两个按钮); "p": prompt, 输入框

@param fn Function(text?) 回调函数,当点击确定按钮时调用。当type="p" (prompt)时参数text为用户输入的内容。

@param opt Object. 可选项。 timeoutInterval表示几秒后自动关闭对话框。defValue用于输入框(type=p)的缺省值.

使用jQuery easyui弹出提示对话框.

示例:

// 信息框,3s后自动点确定
app_alert("操作成功", function () {
    WUI.showPage("pageGenStat");
}, {timeoutInterval: 3000});

// 错误框
app_alert("操作失败", "e");

// 确认框(confirm, 确定/取消)
// 仅当点击“确定”按钮才会进入回调函数。如果想处理取消事件,可使用opt.onCancel()回调
app_alert("立即付款?", "q", function () {
    WUI.showPage("#pay");
});

// 提示输入框(prompt)
// 仅当点击“确定”按钮且输入值非空时才会进入回调函数。可使用opt.defValue指定缺省值
app_alert("输入要查询的名字:", "p", function (text) {
    callSvr("Book.query", {cond: "name like '%" + text + "%'"});
});

@fn app_confirm(msg, fn?)

@param fn Function(isOk). 用户点击确定或取消后的回调。

使用jQuery easyui弹出确认对话框.

@fn app_show(msg, title?)

使用jQuery easyui弹出对话框.

@fn app_progress(value, msg?)

@param value 0-100间数值.

显示进度条对话框. 达到100%后自动关闭.

注意:同一时刻只能显示一个进度条。

@fn makeLinkTo(dlg, id, text?=id, obj?)

生成一个链接的html代码,点击该链接可以打开指定对象的对话框。

示例:根据订单号,生成一个链接,点击链接打开订单详情对话框。

var orderId = 101;
var html = makeLinkTo("#dlgOrder", orderId, "订单" + orderId);

(v5.1)
示例:如果供应商(obj=Supplier)和客户(obj=Customer)共用一个对话框BizPartner,要显示一个id=101的客户,必须指定obj参数:

var html = makeLinkTo("#dlgBizPartner", 101, "客户-101", "Customer");

点击链接将调用

WUI.showObjDlg("#dlgBizPartner", FormMode.forSet, {id: 101, obj: "Customer"};

@fn tryAutoLogin(onHandleLogin, reuseCmd?)

@param onHandleLogin Function(data). 调用后台login()成功后的回调函数(里面使用this为ajax options); 可以直接使用WUI.handleLogin

@param reuseCmd String. 当session存在时替代后台login()操作的API, 如"User.get", "Employee.get"等, 它们在已登录时返回与login相兼容的数据. 因为login操作比较重, 使用它们可减轻服务器压力.

@return Boolean. true=登录成功; false=登录失败.

该函数一般在页面加载完成后调用,如

function main()
{
    $.extend(WUI.options, {
        appName: APP_NAME,
        title: APP_TITLE,
        onShowLogin: showDlgLogin
    });

    WUI.tryAutoLogin(WUI.handleLogin, "Employee.get");
}

$(main);

该函数同步调用后端接口。如果要异步调用,请改用tryAutoLoginAsync函数,返回Deferred对象,resolve表示登录成功,reject表示登录失败。

@fn handleLogin(userInfo)

@param userInfo 调用login/Employee.get等接口返回的用户信息数据。

处理login相关的操作, 如设置g_data.userInfo, 保存自动登录的token等等.

(v5.5) 如果URL中包含hash(即"#pageIssue"这样),且以"#page"开头,则登录后会自动打开同名的列表页(如"pageIssue"页面)。

@var dfdLogin

用于在登录完成状态下执行操作的Deferred/Promise对象。
示例:若未登录,则在登录后显示消息;若已登录则直接显示消息

WUI.dfdLogin.then(function () {
    app_show("hello");
});

@fn initClient(param = null)

一般在进入页面时,同步地调用后端initClient接口,获取基本配置信息。
此后可通过g_data.initClient取这些配置。

若指定param参数(JS对象,如{token: 123}),则作为POST参数调用initClient接口.

@class Plugins

@fn Plugins.exists(pluginName)

@fn Plugins.list()

@fn setApp(opt)

@see options

TODO: remove. use $.extend instead.

@fn logout(dontReload?=0)

@param dontReload 如果非0, 则注销后不刷新页面.

注销当前登录, 成功后刷新页面(除非指定dontReload=1)
返回logout调用的deferred对象

@fn tabClose(idx?)

关闭指定idx的标签页。如果未指定idx,则关闭当前标签页.

@fn getActivePage()

返回当前激活的逻辑页jpage,注意可能为空: jpage.size()==0。

@fn showLoading()

@fn hideLoading()

@var tabMain

标签页组件。为jquery-easyui的tabs插件,可以参考easyui文档调用相关命令进行操作,如关闭当前Tab:

var jtab = WUI.tabMain.tabs("getSelected");
var idx = WUI.tabMain.tabs("getTabIndex", jtab);
WUI.tabMain.tabs("close", idx);

注:要关闭当前Tab,可以直接用WUI.tabClose().

@var PageHeaderMenu

页面上方标题栏的右键菜单

扩展示例:

WUI.PageHeaderMenu.items.push('<div id="reloadUiMeta">刷新Addon</div>');
WUI.PageHeaderMenu.reloadUiMeta = function () {
    UiMeta.reloadUiMeta();
}

@module jquery-mycombobox

1 用url选项加载下拉列表

2 用jdEnumMap选项指定下拉列表

3 用loadFilter调整返回数据

4 动态列表 - setOption

4.1 动态修改固定下拉列表

4.2 旧方案(不建议使用)

5 级联列表支持

6 自动感知对象变动并刷新列表

7 验证要求必填

@fn jQuery.fn.mycombobox (opt?)

@key .my-combobox 关联选择框

@var ListOptions 定义关联选择框的数据源

@param opt.force ?=false 如果为true, 则调用时强制重新初始化。默认只初始化一次。

关联选择框组件。

用法:先定义select组件:

<select name="empId" class="my-combobox" data-options="valueField: 'id', ..."></select>

通过函数参数opt或组件属性data-options可设置选项: { url, formatter(row), loadFilter(data), valueField, textField, jdEnumMap/jdEnumList }

初始化:

var jo = $(".my-combobox").mycombobox();

(v6) 使用参数opt直接指定参数,比使用data-options指定参数优先级高:

var jo = $(".my-combobox").mycombobox({
    url: WUI.makeUrl("dbinst"),
    // dbinst返回数组示例`["aa","bb"]`, 转为`[{name:"aa"}, {name:"bb"}]`才可正常显示. i
    // 数组每1项须是个对象,前两个字段分别用作value和text,如果只有1个字段,则value和text均是这个字段;也可通过valueField和textField分别指定字段名。
    loadFilter: function (data) {
        return $.map(data, function (e) {
            return {name: e};
        });
    }
});

注意:使用WUI.showPage或WUI.showDlg显示的逻辑页或对话框中如果有my-combobox组件,会自动初始化,无须再调用上述代码。

操作:

特性:

注意:

@param opt {url, jdEnumMap/jdEnumList, formatter, textField, valueField, loadFilter, urlParams, isLoaded_, url_, emptyText}

@param opt.url 动态加载使用的url,或一个返回URL的函数(这时会调用opt.url(opt.urlParams)得到实际URL,并保存在opt.url_中)

所以要取URL可以用

var opt = WUI.getOptions(jo);
url = opt.url_ || opt.url;

@param opt.emptyText 设置首个空行(值为null)对应的显示文字。

1 用url选项加载下拉列表

例如,想显示所有员工(Employee)的下拉列表,绑定员工编号字段(id),显示是员工姓名(name):

分派给 <select name="empId" class="my-combobox" data-options="url:WUI.makeUrl('Employee.query', {res:'id,name',pagesz:-1})"></select>

(v6)也可以用input来代替select,组件会自动处理.

注意查询默认是有分页的(页大小一般为20条),用参数{pagesz:-1}使用服务器设置的最大的页大小(-1表示使用后端默认的pagesz,后端可使用maxPageSz参数调节)。
为了精确控制返回字段与显示格式,data-options可能更加复杂,习惯上定义一个ListOptions变量包含各种下拉框的数据获取方式,便于多个页面上共享,像这样:

<select name="empId" class="my-combobox" data-options="ListOptions.Emp()"></select>

var ListOptions = {
    // ListOptions.Emp()
    Emp: function () {
        var opts = {
            url: WUI.makeUrl('Employee.query', {
                res: 'id,name,uname',
                cond: 'storeId=' + g_data.userInfo.storeId,
                pagesz:-1
            }),
            formatter: function (row) { return row.name + '(' + row.uname + ')'; }
        };
        return opts;
    },
    ...
};

返回对象的前两个字段被当作值字段(valueField)和显示字段(textField),上例中分别是id和name字段。
如果返回对象只有一个字段,则valueField与textField相同,都是这个字段。
如果指定了formatter,则显示内容由它决定,textField此时无意义。

可以显式指定这两个字段,如:

var opts = {
    valueField: "id",
    textField: "name",
    url: ...
}

示例2:下拉框绑定User.city字段,可选项为该列已有的值:

<select name="city" class="my-combobox" data-options="ListOptions.City()"></select>

var ListOptions = {
    City: function () {
        var opts = {
            url: WUI.makeUrl('User.query', {
                res: 'city',
                cond: 'city IS NOT NULL'
                distinct: 1,
                pagesz:-1
            })
        };
        return opts;
    },
    ...
};

(v5.2) url还可以是一个函数。如果带一个参数,一般用于动态列表级联列表。参考后面相关章节。

2 用jdEnumMap选项指定下拉列表

也支持通过key-value列表用jdEnumMap选项或jdEnumList选项来初始化下拉框,如:

订单状态: <select name="status" class="my-combobox" data-options="jdEnumMap:OrderStatusMap"></select>
或者:
订单状态: <select name="status" class="my-combobox" data-options="jdEnumList:'CR:未付款;CA:已取消'"></select>
或者:(key-value相同时, 只用';'间隔)
订单状态: <select name="status" class="my-combobox" data-options="jdEnumList:'未付款;已取消'"></select>

其中OrderStatusMap定义如下:

var OrderStatusMap = {
    "CR": "未付款",
    "CA": "已取消"
};

3 用loadFilter调整返回数据

另一个例子:在返回列表后,可通过loadFilter修改列表,例如添加或删除项:

<select name="brandId" class="my-combobox" data-options="ListOptions.Brand()" ></select>

JS代码ListOptions.Brand:

var ListOptions = {
    ...
    // ListOptions.Brand()
    Brand: function () {
        var opts = {
            url:WUI.makeUrl('queryBrand', {res: "id,name", pagesz:-1}),
            loadFilter: function(data) {
                data.unshift({id:'0', name:'所有品牌'});
                return data;
            }
        };
        return opts;
    }
};

更简单地,这个需求还可以通过同时使用jdEnumMap和url来实现:

var ListOptions = {
    ...
    // ListOptions.Brand()
    Brand: function () {
        var opts = {
            url:WUI.makeUrl('queryBrand', {res: "id,name", pagesz:-1}),
            jdEnumMap: {0: '所有品牌'}
        };
        return opts;
    }
};

注意:jdEnumMap指定的固定选项会先出现。

4 动态列表 - setOption

(v6) url选项使用函数,之后调用loadOptions方法刷新

示例:在安装任务明细对话框(dlgTask)中,根据品牌(brand)过滤显示相应的门店列表(Store).

var ListOptions = {
    // 带个cond参数,为query接口的查询条件参数,支持 "brand='xxx'" 或 {brand: 'xxx'}两种格式。
    Store: function (cond) {
        var opts = {
            valueField: "id",
            textField: "name",
            url: WUI.makeUrl('Store.query', {
                res: 'id,name',
                cond: cond,
                pagesz: -1
            },
            formatter: function (row) { return row.id + "-" + row.name; }
        };
        return opts;
    }
};

在明细对话框HTML中不指定options而是代码中动态设置:

<form>
    品牌 <input name="brand">
    门店 <select name="storeId" class="my-combobox"></select>
</form>

对话框初始化函数:在显示对话框或修改品牌后刷新门店列表

function initDlgTask()
{
    ...

    $(frm.brand).on("change", function () {
        if (this.value) {
            // 用setOption动态修改设置。注意trigger函数第二个参数须为数组,作为参数传递给用on监听该事件的处理函数。
            $(frm.storeId).trigger("setOption", [ ListOptions.Store({brand: this.value}) ]);
        }
    });

    function onShow() {
        $(frm.brand).trigger("change");
    }
}
4.1 动态修改固定下拉列表

(v6) 示例:根据type决定下拉列表用哪个,通过setOption来设置。

function onBeforeShow(ev, formMode, opt) {
    var type = opt.objParam && opt.objParam.type || opt.data && opt.data.type;

    var comboOpt = type == "入库" ? { jdEnumMap: MoveTypeMap } :
        type == "出库"? { jdEnumMap: MoveTypeMap2 } : null
    jdlg.find("[name=moveType]").trigger("setOption", [comboOpt]);
}

如果setOption给的参数是null,则忽略不处理。

4.2 旧方案(不建议使用)

(v5.2起, v6前) url选项使用函数,之后调用loadOptions方法刷新:

var ListOptions = {
    Store: function () {
        var opts = {
            valueField: "id",
            textField: "name",
            // !!! url使用函数指定, 之后手工给参数调用loadOptions方法刷新 !!!
            url: function (brand) {
                return WUI.makeUrl('Store.query', {
                    res: 'id,name',
                    cond: "brand='" + brand + "'",
                    pagesz: -1
                })
            },
            formatter: function (row) { return row.id + "-" + row.name; }
        };
        return opts;
    }
};

在明细对话框HTML中:

<form>
    品牌 <input name="brand">
    门店 <select name="storeId" class="my-combobox" data-options="ListOptions.Store()"></select>
</form>

对话框初始化函数:在显示对话框或修改品牌后刷新门店列表

function initDlgTask()
{
    ...

    $(frm.brand).on("change", function () {
        if (this.value)
            $(frm.storeId).trigger("loadOptions", this.value);
    });

    function onShow() {
        $(frm.brand).trigger("change");
    }
}

5 级联列表支持

(v5.2引入, v6使用新方案) 与动态列表机制相同。

示例:缺陷类型(defectTypeId)与缺陷代码(defectId)二级关系:选一个缺陷类型,缺陷代码自动刷新为该类型下的代码。
在初始化时,如果字段有值,下拉框应分别正确显示。

在一级内容切换时,二级列表自动从后台查询获取。同时如果是已经获取过的,缓存可以生效不必反复获取。
双击仍支持刷新。

对话框上HTML如下:(defectId是用于提交的字段,所以用name属性;defectTypeId不用提交,所以用了id属性)

<select id="defectTypeId" class="my-combobox" data-options="ListOptions.DefectType()" style="width:45%"></select>
<select name="defectId" class="my-combobox" data-options="" style="width:45%"></select>

defectId上暂时不设置,之后传参动态设置。

其中,DefectType()与传统设置无区别,在Defect()函数中,应设置url为一个带参函数:

var ListOptions = {
    DefectType: function () {
        var opts = {
            valueField: "id",
            textField: "code",
            url: WUI.makeUrl('Defect.query', {
                res: 'id,code,name',
                cond: 'typeId is null',
                pagesz: -1
            }),
            formatter: function (row) { return row.code + "-" + row.name; }
        };
        return opts;
    },
    // ListOptions.Defect
    Defect: function (typeId) {
        var opts = {
            valueField: "id",
            textField: "code",
            url: WUI.makeUrl('Defect.query', {
                res: 'id,code,name',
                cond: "typeId=" + typeId,
                pagesz: -1
            },
            formatter: function (row) { return row.code + "-" + row.name; }
        };
        return opts;
    }
}

在对话框上设置关联动作,调用setOption事件:

$(frm.defectTypeId).on("change", function () {
    var typeId = $(this).val();
    if (typeId)
        $(frm.defectId).trigger("setOption", [ ListOptions.Defect(typeId) ]);
});

注意jQuery的trigger发起事件函数第二个参数须为数组。

对话框加载时,手工设置defectTypeId的值:

function onShow() {
    $(frm.defectTypeId).val(defectTypeId).trigger("change");
}

6 自动感知对象变动并刷新列表

假如某mycombobox组件查询Employee对象列表。当在Employee页面新建、修改、删除对象后,回到组件的页面,点击组件时将自动刷新列表。
(wui-combogrid也具有类似功能)

7 验证要求必填

(v6) my-combobox继承easyui-validatebox,通过添加requried属性来指定必填项,显示时会在字段后自动加红色星号:

<select name="status" class="my-combobox" data-options="jdEnumMap:OrderStatusMap" required></select>

默认的select组件不支持设置必填选项(required属性,即使加easyui-validatebox类也不行;easyui-combobox可支持验证,但它默认并未限制选项必须在列表中),加上my-combobox类即可解决:

<select name="status" class="my-combobox" required>
    <option value="CR">新创建</option>
    <option value="RE">已完成</option>
</select>

旧用法(不建议使用):

<select name="status" class="my-combobox easyui-validatebox" data-options="jdEnumMap:OrderStatusMap, required:true"></select>

@module JdcloudExt

框架扩展功能或常用函数.

@key .wui-upload

1 右键菜单

2 音频等文件上传

3 压缩参数

4 动态设置选项

5 定制上传接口

6 存储小图还是大图

对话框上的上传文件组件。

用于在对象详情对话框中,展示关联图片字段。图片可以为单张或多张。
除显示图片外,也可以展示其它用户上传的文件,如视频、文本等。

预览图样式在style.css中由 .wui-upload img定义。
点击一张预览图,如果有jqPhotoSwipe插件,则全屏显示图片且可左右切换;否则在新窗口打开大图。

示例:只需要标注wui-upload类及指定data-options,即可实现图片预览、上传等操作。

<table>
    <tr>
        <td>上传多图</td>
        <td class="wui-upload">
            <input name="pics">
        </td>
    </tr>
    <tr>
        <td>上传单图</td>
        <td class="wui-upload" data-options="multiple:false">
            <input name="picId">
        </td>
    </tr>
    <tr>
        <td>上传附件</td>
        <td class="wui-upload" data-options="pic:false,fname:'attName'"> <!-- v5.5: 显示使用虚拟字段attName,须后端提供,也可以用 fname=1将显示内容保存到atts字段 -->
            <input name="atts">
        </td>
    </tr>
    <tr>
        <td>上传单个附件</td>
        <td class="wui-upload" data-options="multiple:false,pic:false,fname:'attName'">
            <input name="attId">
        </td>
    </tr>
</table>

组件会自动添加预览区及文件选择框等,完整的DOM大体如下:

<div class="wui-upload">
    <input name="atts">
    <div class="imgs"></div>
    <input type="file" multiple>
    <p class="hint"><a href="javascript:;" class="btnEditAtts">编辑</a></p>
</div>

其中imgs为预览区,内部DOM结构为 .imgs - a标签 - img或p标签
在上传图片或文件后,会在imgs下创建<a>标签,对于图片a标签里面是img标签,否则是p标签。
a标签上数据如下:

在a下的img标签上,有以下数据:

@param opt.multiple=true 设置false限制只能选一张图。

@param opt.nothumb=false 设置为true表示不生成缩略图,且不做压缩(除非指定maxSize参数,这时不生成缩略图,但仍然压缩)。

<td class="wui-upload" data-options="nothumb:true">...</td>

@param opt.pic=true 设置为false,用于上传视频或其它非图片文件

如果为false, 在.imgs区域内显示文件名链接而非图片。

@param opt.fname 是否显示文件名。默认为0。目前不用于图片,只用于附件(即pic=false时有效)。

(v5.5)

示例:用attId字段保存单个附件,并显示附件名

由于attId是数值字段,不可存额外字符串信息,所以不能设置fname=1。
这时在后端做一个虚拟字段如attName,格式为"{attId}:{fileName}":

    protected $vcolDefs = [
        [
            "res" => ["concat(att.id,':',att.orgName) attName"],
            "join" => "LEFT JOIN Attachment att ON att.id=t0.attId",
            "default" => true
        ]
        ...
    ];

列表页中展示使用虚拟字段attName而非attId:

        <th data-options="field:'attName', formatter:Formatter.atts">模板文件</th>

在详情对话框中指定fname为虚拟字段"att":

        <td>模板文件</td>
        <td class="wui-upload" data-options="multiple:false,pic:false,fname:'attName'">
            <input name="attId">
        </td>

示例:用atts存多个附件。

这时,可设置fname=1,即把文件名也存到atts字段。
也可以设置fname='attName'(虚拟字段),其格式为"{attId}:{filename},{attId2}:{filename2}",后端实现参考如下(使用find_in_set):

        [
            "res" => ["(SELECT group_concat(concat(att.id,':',att.orgName)) FROM Attachment att WHERE find_in_set(id, t0.atts)) attName"],
            "default" => true
        ]

@param opt.manual=false 是否自动上传提交

默认无须代码即可自动上传文件。如果想要手工操控,可以触发submit事件,
示例:在dialog的validate事件中先确认提示再上传,而不是直接上传:

HTML:

<div class="wui-upload" data-options="manual:true">...</div>

JS:

jdlg.on("validate", onValidate);
function onValidate(ev)
{
    var dfd = $.Deferred();
    app_alert("确认上传?", "q", function () {
        var dfd1 = WUI.triggerAsync(jdlg.find(".wui-upload"), "submit");
        dfd1.then(doNext);
    });
    // dialog的validate方法支持异步,故设置ev.dfds数组来告知调用者等异步操作结束再继续
    ev.dfds.push(dfd.promise());

    function doNext() {
        dfd.resolve();
    }
}

1 右键菜单

@param opt.menu 设置右键菜单

在预览区右键单击会出现菜单,默认有“删除”菜单。
示例:商品可以上传多张照片,其中选择一张作为商品头像。
我们在右键中添加一个“设置为缺省头像”菜单,点击后将该图片圈出,将其图片编号保存到字段中。
数据结构为:

@Item: id, picId, pics

上面表Item中,picId为头像的图片ID,pics为逗号分隔的图片列表。

HTML: 在data-options中指定菜单的ID和显示文字。缺省头像将添加"active"类:

<style>
.wui-upload img.active {
    border: 5px solid red;
}
</style>

<tr>
    <td>门店照片</td>
    <td class="wui-upload" data-options="menu:{mnuSetDefault:'设置为缺省头像'}">
        <input name="pics">
        <input type="input" style="display:none" name="picId">
    </td>
</tr>

在右键点击菜单时,wui-upload组件会触发onMenu事件,其中参数item为当前预览项(.imgs区域下的a标签,在它下面才是img或p标签)

// 在initDlgXXX中:
jdlg.on("menu", onMenu);
function onMenu(ev, menuId, item)
{
    if (menuId == "mnuSetDefault") {
        var ja = $(item);
        if (ja.attr("attId")) {
            ja.closest(".wui-upload").find("img").removeClass("active");
            var jimg = ja.find("img");
            jimg.addClass("active");

            frm = jdlg.find("form")[0];
            frm.picId.value = jimg.attr("picId");
        }
    }
}
// 高亮显示选中的头像picId。
// 注意:要用jdlg而不是jfrm的show事件。否则wui-upload尚未初始化完成
jdlg.on("show", function (ev, formMode, initData) {
    if (initData && initData.picId) {
        jdlg.find(".wui-upload img[picId=" + initData.picId + "]").addClass("active");
    }
});

2 音频等文件上传

@param opt.accept 指定可上传的文件类型

示例:上传单个音频文件,如m4a, mp3等格式。

<td>文件</td>
<td class="wui-upload" data-options="multiple:false,pic:false,accept:'audio/*'">
    <input name="attId">
    <p class="hint">要求格式m4a,mp3,wav; 采样率为16000</p>
</td>

3 压缩参数

@param opt.maxSize ?=1280 指定压缩后图片的最大长或宽

@param opt.quality ?=0.8 指定压缩质量, 一般不用修改.

示例:默认1280像素不够, 增加到2000像素:

<tr>
    <td>图片</td>
    <td class="wui-upload" data-options="maxSize:2000">
        <input name="pics">
    </td>
</tr>

4 动态设置选项

示例:在dialog的beforeshow事件回调中,根据情况设置upload组件的选项,如是否压缩图片:

// function initDlgXXX
//
jdlg.on("beforeshow", onBeforeShow);

function onBeforeShow(ev, formMode, opt) 
{
    var objParam = opt.objParam;
    var jo = jpage.find(".picId");
    // 获取和动态设置选项:
    var uploadOpt = WUI.getOptions(jo);
    uploadOpt.nothumb = (objParam.type === "A");
}

5 定制上传接口

6 存储小图还是大图

jdcloud传统风格是在上传图片后(upload接口),存储返回的小图编号(thumbId), 通过att(thumbId)访问大图。

WUI.options.useNewThumb=1时为新风格,即存储upload接口返回的大图编号(id),通过att(id, thumb=1)访问小图。

@key .wui-checkList

用于在对象详情对话框中,以一组复选框(checkbox)来对应一个逗号分隔式列表的字段。
例如对象Employee中有一个“权限列表”字段perms定义如下:

perms:: List(perm)。权限列表,可用值为: item-上架商户管理权限, emp-普通员工权限, mgr-经理权限。

现在以一组checkbox来在表达perms字段,希望字段中有几项就将相应的checkbox选中,例如值"emp,mgr"表示同时具有emp与mgr权限,显示时应选中这两项。
定义HTML如下:

<tr>
    <td>权限</td>
    <td class="wui-checkList">
        <input type="hidden" name="perms">
        <label><input type="checkbox" value="emp" checked>员工(默认)</label><br>
        <label><input type="checkbox" value="item">上架商品管理</label><br>
        <label><input type="checkbox" value="mgr">经理</label><br>
    </td>
</tr>

wui-checkList块中包含一个hidden对象和一组checkbox. hidden对象的name设置为字段名, 每个checkbox的value字段设置为每一项的内部名字。

@key .wui-labels

标签字段(labels)是空白分隔的一组词,每个词是一个标签(label)。

一般会在字段下方将常用标签列出供用户选择,点一下标签则追加到文本框中,再点一下删除该标签项。多个标签以空格分隔。

(v6) 可以设置选项opt.simple=true,这时如果单击一个标签,则直接填写(而不是追加)到文本框中,类似于单选。

示例1:列出各种类型,点一下类型标签就追加到对话框,再点一下会删除该项。

<tr>
    <td>标签</td>
    <td class="wui-labels">
        <input name="label" >
        <p class="hint">企业类型:<span class="labels" dfd="DlgStoreVar.onGetLabel()"></span></p>
        <p class="hint">行业标签:<span class="labels">IT 金融 工业</span></p>
        <p class="hint">位置标签:<span class="labels">一期 二期 三期 四期</span></p>
    </td>
</tr>

具有CSS类"labels"的组件,内容以空白分隔的多个标签,如`IT 金融 工业"。

本例中dfd属性"DlgStoreVar.onGetLabel()"是个函数调用,它返回的是一个Deferred对象,这样可以实现异步获取再设置标签列表,示例:(在dlgStore.js中)

function initDlgStore() {
    // 按惯例,只被Xx页面使用的变量可放在dlgXx.js中的DlgXxVar中,而会被其它页面调用的变量则放在全局应用store.js中的DlgXx中(称为页面接口)。
    window.DlgStoreVar = {
        onGetLabel: function () {
            var dfd = $.Deferred();
            callSvr("Conf.query", {cond: "name='企业分类'", fmt: "one", res: "value"}, function (data) {
                DlgStoreVar.dfdLabel.resolve(data.value);
            })
            return dfd;
        }
    };
    ...
}

上面的DlgStoreVar.onGetLabel也可以直接返回标签列表,如:

    window.DlgStoreVar = {
        onGetLabel: function () {
            return "标签1 标签2";
        }
    }

在第一次打开页面时会调用onGetLabel函数,设置标签列表。

(v6) 如果想每次打开页面都调用,只需要稍加修改,直接为dfd指定函数DlgStoreVar.onGetLabel(而不是函数调用后的返回值DlgStoreVar.onGetLabel()):

    <p class="hint">企业类型:<span class="labels" dfd="DlgStoreVar.onGetLabel"></span></p>

(v6) 示例1-1:可以为不良品(Fault)设置标签,可通过系统配置项"Cinf.faultLabel"来配置可用的标签,即后端提供Cinf.getValue({name: "faultLabel"})接口获取可用标签列表(以空格分隔)

在dlgFault.html中设置标签字段:

    <tr>
        <td>标签</td>
        <td class="wui-labels">
            <input name="label">
            <p class="hint"><span class="labels" dfd="DlgFaultVar.onGetLabel"></span></p>
        </td>
    </tr>

注意dfd属性直接指定为函数DlgFaultVar.onGetLabel,也就是每次打开页面都会执行它:这意味着当修改系统配置项faultLabel后,无须刷新系统,重新打开不良品对话框就可以点选新标签了。

在dlgFault.js中定义动态获取标签:

window.DlgFaultVar = {
    onGetLabel: function () {
        return callSvr("Cinf.getValue", {name: "faultLabel"});
    }
}

注意:函数里比上例简化了很多,因为callSvr返回是就是Defered对象,而且由于Cinf.getValue接口刚好返回的数据格式就是"标签1 标签2"字符串,所以不用再新建一个Defered对象在处理格式转换后做resolve了。

示例2:设置配置项,并配以说明和示例

<tr>
    <td>配置项名称</td>
    <td class="wui-labels">
        <input name="name" class="easyui-validatebox" required>
        <div class="hint">可选项和示例值:
            <p class="easyui-tooltip" title="在移动端提交缺陷问题时,可从下拉列表中选择问题类型,就是在此处配置的,多个值以英文分号分隔。"><span class="labels">常见问题</span> 内饰;轮胎</p>
            <p class="easyui-tooltip" title="多个值以英文空格分隔"><span class="labels">集市品类</span> 办公用品 书籍 卡票券</p>
            <p class="easyui-tooltip" title="格式为`姓名,电话`"><span class="labels">会议室预订联系人</span> Candy,13917091068</p>
        </div>
    </td>
</tr>

示例3:(v6) 推荐项为单选(simple模式),且标签可以显示(text)与实际值(value)不同。

    <tr>
        <td>URL地址</td>
        <td class="wui-labels" data-options="simple:true">
            <input name="url" value="http://oliveche.com/mes/">
            <p class="hint">
                <span class="labels">生产环境|http://192.168.10.23/mes/ 测试|http://oliveche.com/mes/</span>
            </p>
        </td>
    </tr>

@key #menu

管理端功能菜单,以"menu"作为id:

<div id="menu">
    <div class="menu-expand-group">
        <a class="expanded"><span><i class="fa fa-pencil-square-o"></i>主数据管理</span></a>
        <div class="menu-expandable">
            <a href="#pageCustomer">客户管理</a>
            <a href="#pageStore">门店管理</a>
            <a href="#pageVendor">供应商管理</a>
        </div>
    </div>
    <!-- 单独的菜单项示例 -->
    <a href="javascript:;" onclick="WUI.showDlg('#dlgImport',{modal:false})"><span><i class="fa fa-pencil-square-o"></i>批量导入</span></a>
    <a href="javascript:;" onclick="showDlgChpwd()"><span><i class="fa fa-user-times"></i>修改密码</span></a>
</div>

菜单组由menu-expand-group标识,第一个a为菜单组标题,可加"expanded"类使其默认展开。
图标使用font awesome库,由<i class="fa fa-xxx"></i>指定,图标查询可参考 http://www.fontawesome.com.cn/faicons/https://fontawesome.com/icons

@fn getMenuTree(jo=$("#menu")|opt)

返回主菜单的树型数组:[ {name, perm, @children} ],可通过opt.filter来修改返回结构。

可以用tree组件展示,如:

// 转为 [{text, children}] 结构
var tree = WUI.getMenuTree({
    filter: function (e) {
        var text = e.name;
        if (e.name != e.perm)
            text += "(" + e.perm + ")";
        return { text: text, children: e.children }
    }
});
jo.tree({
    data: tree,
    checkbox: true
});

可以用treegrid组件展示,如:

var tree = WUI.getMenuTree();
jtbl.treegrid({
    data: tree,
    idField: "name",
    treeField: "name",
    checkbox: true,
    columns:[[
        {title:'菜单项',field:'name',formatter: function (v, row) {
            if (v == row.perm)
                return v;
            return v + "(" + row.perm + ")";
        }},
    ]]
});

@key .wui-combogrid

1 markRefresh 标记下次打开时刷新列表

2 动态修改下拉列表

3 使用gn函数访问组件

可搜索的下拉列表。
示例:在dialog上,填写门店字段(填写Id,显示名字),从门店列表中选择一个门店。

<form my-obj="Task" title="安装任务" wui-script="dlgTask.js" my-initfn="initDlgTask">
    <tr>
        <td>门店</td>
        <td>
            <input class="wui-combogrid" name="storeId" data-options="ListOptions.StoreGrid">
        </td>
    </tr>
</form>

选项定义如下:

ListOptions.StoreGrid = {
    jd_vField: "storeName",
    jd_dlgForAdd: "#dlgStore",
    // jd_qsearch: "name", // 模糊匹配字段
    panelWidth: 450,
    width: '95%',
    textField: "name",
    columns: [[
        {field:'id',title:'编号',width:80},
        {field:'name',title:'名称',width:120}
    ]],
    url: WUI.makeUrl('Store.query', {
        res: 'id,name',
    })
};

属性请参考easyui-combogrid相关属性。wui-combogrid扩展属性如下:

在wui-combogrid的输入框中输入后会自动进行模糊查询,展示匹配的数据列表;
选项jd_qsearch用于直接指定模糊查询的字段列表;如果不指定的话,则须由后端指定,否则无法模糊查询。

未指定jd_qsearch选项时,调用接口示例为callSvr("Store.query", {res:"id,name", q="张三"}).
须由筋斗云后端指定qsearch字段,示例如下:(详细可参考后端手册, 搜索qsearch):

class AC2_Xxx
{
    protected function onQuery() {
        $this->qsearch("name,phone", param("q"));
    }
}

指定jd_qsearch时,调用接口示例为: callSvr("Store.query", {res:"id,name", qserach:"name:张三"}).

在选择一行并返回时,它会触发choose事件:

// 注意要取combogrid对象要用comboname! 而不是用 "[name=storeId]"(原始的input已经变成一个hidden组件,只存储值)
var jo = jdlg.find("[comboname=storeId]"); 
jo.on("choose", function (ev, row) {
    console.log('choose row: ', row);
    ...
});

jo是easyui-combogrid,可以调用它的相应方法,如禁用和启用它:

jo.combogrid("disable");
jo.combogrid("enable");

特别逻辑:

示例2:简单的情况,选择时只用名字,不用id。

// var ListOptions 定义中:
// 只用name不用id
CateGrid: {
    jd_vField: "category",
    jd_showId: false,
    // jd_qsearch: "name,fatherName",
    panelWidth: 450,
    width: '95%',
    idField: "name",
    textField: "name",
    columns: [[
        {field:'name',title:'类别',width:120},
        {field:'fatherName',title:'父类别',width:120},
    ]],
    url: WUI.makeUrl('Category.query', {
        res: 'id,name,fatherName'
    })
}

在dialog中:

    <input class="wui-combogrid" name="category" data-options="ListOptions.CateGrid">

设置方法:

1 markRefresh 标记下次打开时刷新列表

(v5.5) 与my-combobox类似,组件会在其它页面更新对象后自动刷新列表。
外部想要刷新组件列表,可以触发markRefresh事件:

jo = jdlg.find("[comboname=xxxId]");
jo.trigger("markRefresh", obj); // obj是可选的,若指定则仅当obj匹配组件对应obj时才会刷新。

2 动态修改下拉列表

(v6) 与my-combobox方法相同,重新设置选项:

jo = jdlg.find("[comboname=xxxId]");
jo.trigger("setOption", opt);

动态修改组件选项示例:

// 比如每打开对话框时,根据type动态显示列表。可在dialog的beforeShow事件中编码:
var cond = type == 'A'? xx: yy...;
// 取出选项,动态修改url
var jo = jdlg.find("[comboname=categoryId]");
jo.trigger("setOption", ListOptions.CategoryGrid(cond));

3 使用gn函数访问组件

设置值:

jdlg.gn("userId").val(10);
jdlg.gn("userId").val([10, "用户1"]); // 如果传数组,则同时设置值和显示文本

设置状态:

jdlg.gn("userId").visible(true)
    .readonly(true)
    .disabled(true);

注意gn函数支持链式调用。

重置选项:

jdlg.gn("userId").setOption(ListOptions.UserGrid({phone: "~*9204"}));

@key .combo-f

支持基于easyui-combo的表单扩展控件,如 combogrid/datebox/datetimebox等, 在使用WUI.getFormData时可以获取到控件值.

示例:可以在对话框或页面上使用日期/日期时间选择控件:

<input type="text" name="startDt" class="easyui-datebox">
<input type="text" name="startTm" data-options="showSeconds:false" class="easyui-datetimebox">

form提交时能够正确获取它们的值:
var d = WUI.getFormData(jfrm); // {startDt: "2019-10-10", startTm: "2019-10-10 10:10"}

而且在查询模式下,日期等字段也不受格式限制,可输入诸如"2019-10", "2019-1-1~2019-7-1"这样的表达式。

@fn toggleCol(jtbl, col, show)

显示或隐藏datagrid的列。示例:

WUI.toggleCol(jtbl, 'status', false);

如果列不存在将出错。

@fn toggleFields(jtbl_or_jfrm, showMap)

根据type隐藏datagrid列表或明细页form中的项。示例:

function toggleItemFields(jo, type)
{
    WUI.toggleFields(jo, {
        type: !type,
        status: !type || type!="公告",
        tm: !type || type=="活动" || type=="卡券" || type=="停车券",
        price: !type || type=="集市",
        qty: !type || type=="卡券"
    });
}

列表中调用,控制列显示:pageItem.js

    var type = objParam & objParam.type; // 假设objParam是initPageXX函数的传入参数。
    toggleItemFields(jtbl, type);

明细页中调用,控制字段显示:dlgItem.js

    var type = objParam && objParam.type; // objParam = 对话框beforeshow事件中的opt.objParam
    toggleItemFields(jfrm, type);

@key .wui-field

在隐藏字段时,默认是找到字段所在的行(tr)或标识wui-field类的元素控制其显示或隐藏。示例:

    <tr>
        <td></td>
            <label class="wui-field"><input name="forEnd" type="checkbox" value="1"> 结束打卡</label>
        </td>

        <td></td>
        <td>
            <label class="wui-field"><input name="repairFlag" type="checkbox" value="1"> 是否维修</label>
        </td>
    </tr>

这里一行有两组字段,以wui-field类来指定字段所在的范围。如果不指定该类,则整行(tr层)将默认当作字段范围。
JS控制:(dialog的onShow时)

    WUI.toggleFields(jfrm, {
        forEnd: formMode == FormMode.forSet && !frm.tm1.value,
        repairFlag: g_args.repair
    })

@fn WUI.applyPermission()

@key permission 菜单权限控制

前端通过菜单项来控制不同角色可见项,具体参见store.html中菜单样例。

<div class="perm-mgr" style="display:none">
    <div class="menu-expand-group">
        <a><span><i class="fa fa-pencil-square-o"></i>系统设置</span></a>
        <div class="menu-expandable">
            <a href="#pageEmployee">登录帐户管理</a>
            ...
        </div>
    </div>
</div>

系统默认使用mgr,emp两个角色。一般系统设置由perm-mgr控制,其它菜单组由perm-emp控制。
其它角色则需要在角色表中定义允许的菜单项。

根据用户权限,如"item,mgr"等,菜单中有perm-xxx类的元素会显示,有nperm-xxx类的元素会隐藏

示例:只有mgr权限显示

<div class="perm-mgr" style="display:none"></div>

示例:bx权限不显示(其它权限可显示)

<a href="#pageItem" class="nperm-bx">商品管理</a>

可通过 g_data.hasRole(roles) 查询是否有某一项或几项角色。注意:由于历史原因,hasRole/hasPerm是同样的函数。

var isMgr = g_data.hasRole("mgr"); // 最高管理员
var isEmp = g_data.hasRole("emp"); // 一般管理员
var isAdm = g_data.hasRole("mgr,emp"); // 管理员(两种都行)
var isKF = g_data.hasRole("客服");

自定义权限规则复杂,一般由框架管理,可以用canDo(对象, 权限)函数查询,如:

var bval = WUI.canDo("客户管理"); // 查一个特定对象,名词性
var bval = WUI.canDo(null, "首件确认"); // 查一个特定权限,动词性。(最高)管理员或指定了"*"权限的话,则默认也允许。
var bval = WUI.canDo(null, "维修", false); // 查一个特定权限,动词性,缺省值设置false表示未直接设置就不允许,即使是(最高)管理员或指定了"*"权限也不允许。

WUI.canDo的底层实现是通过g_data.permSet[perm]查询。

@fn WUI.fname(fn)

为fn生成一个名字。一般用于为a链接生成全局函数。

function onGetHtml(value, row) {
    var fn = WUI.fname(function () {
        console.log(row);
    });
    return '<a href="' + fn + '()">' + value + '</a>';
}

或:

function onGetHtml(value, row) {
    return WUI.makeLink(value, function () {
        console.log(row);
    });
}

@see makeLink

生成一个A链接,显示text,点击运行fn.
用于为easyui-datagrid cell提供html.

<table>
    ...
    <th data-options="field:'orderCnt', sortable:true, sorter:intSort, formatter:ItemFormatter.orderCnt">订单数/报名数</th>
</table>

定义formatter:

var ItemFormatter = {
    orderCnt: function (value, row) {
        if (!value)
            return value;
        return WUI.makeLink(value, function () {
            var objParam = {type: row.type, itemId: row.id};
            WUI.showPage("pageOrder", "订单-" + objParam.itemId, [ objParam ]);
        });
    },
};

@see fname

@key toolbar 工具栏扩展菜单项

@key toolbar-qsearch

工具栏-模糊查询。支持指定查询字段。

// pageXx.js function initPageXx
var dgOpt = {
    ...
    toolbar: WUI.dg_toolbar(jtbl, jdlg, "qsearch"),
};
jtbl.datagrid(dgOpt);

它调用接口callSvr("Xx.query", {q: val})
需要后端支持查询,比如须指定在哪些字段查询:

protected function onQuery() {
    $this->qsearch(["code", "category"], param("q"));
}

(v6) 新用法:也支持前端直接指定字段查询:

var dgOpt = {
    ...
    // 表示在code或category两个字段中模糊查询。
    toolbar: WUI.dg_toolbar(jtbl, jdlg, ["qsearch", "code,category"]),
};
jtbl.datagrid(dgOpt);

它将调用接口callSvr("Xx.query", {qsearch: "字段1,字段2:" + val})

@key toolbar-approve 审批菜单

参数选项:

依赖字段:

可选字段:

示例:显示审批菜单,在发起审批时,若未指定审批人则自动填写审批人

function canApprove_WO(row) {
    return g_data.userInfo.id == row.approveEmpId || g_data.hasRole("mgr,售后审核");
}
var btnApprove = ["approve", {
    obj: "WarrantyOrder", // 最终将调用"WarrantyOrder.set"接口
    canApprove: function (row) {
        return canApprove_WO(row);
    },
    // data: 待保存的新数据(data.approveFlag为新状态), row: 原数据, title: 当前菜单项名称,比如“发起审批”
    onSet: function (row, data, title) {
        // 发起审批时,自动填写审批人
        if (data.approveFlag ==1 && row.approveEmpId == null) {
            var empId = callSvrSync("Employee.query", {fmt: "one?", res: "id", role:"售后审核"});
            if (empId)
                data.approveEmpId = empId;
        }
    }
}];

var dgOpt = {
    ...
    toolbar: WUI.dg_toolbar(jtbl, jdlg, btnApprove),
};
jtbl.datagrid(dgOpt);

做二次开发时,对话框上approveFlag设置示例:

{
    disabled: e => canApprove_WO(e),
    enumMap: ApproveFlagMap,
    styler: Formatter.enumStyler(ApproveFlagStyler),
    desc: "【售后审核】角色或【最高管理员】或指定审批人可审批"
}

对话框上approveEmpId设置示例:

{
    disabled: e => canApprove_WO(e)
}

上面例子中用callSvrSync是使用同步方式来取数据的,更好的写法是直接用异步的callSvr。
onSet回调支持异步,
异步写法1:使用async/await。

    onSet: async function (row, data) {
        if (data.approveFlag ==1 && row.approveEmpId == null) {
            var empId = await callSvr("Employee.query", {fmt: "one?", res: "id", role:"售后审核"});
            if (empId)
                data.approveEmpId = empId;
        }
    }

异步写法2:返回一个Deferred对象(callSvr函数刚好返回Deferred对象)

    onSet: function (row, data) {
        if (data.approveFlag ==1 && row.approveEmpId == null) {
            var dfd = callSvr("Employee.query", {fmt: "one?", res: "id", role:"售后审核"});
            dfd.then(function (empId) {
                if (empId)
                    data.approveEmpId = empId;
            });
            return dfd;
        }
    }

示例:审批时弹出对话框,可输入备注(approveCmt)
在onSet中使用WUI.showDlgByMeta弹出自定义对话框,显然是异步操作,需要返回dfd对象。

function canApprove_WO(row) {
    return g_data.userInfo.id == row.approveEmpId || g_data.hasRole("mgr,售后审核");
}
var btnApprove = ["approve", {
    obj: "WarrantyOrder",
    canApprove: function (row) {
        return canApprove_WO(row);
    },
    // data: 待保存的新数据(data.approveFlag为新状态), row: 原数据, title: 当前菜单项名称,比如“发起审批”

    onSet: function (row, data, title) {
        var dfd = $.Deferred();
        var meta = [
            // title, dom, hint?
            {title: "备注", dom: "<textarea name='approveCmt' rows=5></textarea>", hint: '选填,将添加到备注列表中'}
        ];
        var jdlg = WUI.showDlgByMeta(meta, {
            title: title,
            onOk: async function (data1) {
                // 发起审批时,自动填写审批人
                if (data.approveFlag ==1 && row.approveEmpId == null) {
                    var empId = await callSvr("Employee.query", {fmt: "one?", res: "id", role:"售后审核"});
                    if (empId)
                        data.approveEmpId = empId;
                }
                if (data1.approveCmt) {
                    data.cmts = [
                        {text: data1.approveCmt}
                    ];
                }
                dfd.resolve();
                WUI.closeDlg(jdlg);
            }
        });
        return dfd;
    }
}];

@key .wui-subobj

1 示例:可以增删改查的子表:

2 动态修改选项

3 定制子对象操作按钮toobar

4 添加主对象时检查子表

5 示例2:主表记录添加时不需要展示,添加之后子表/关联表可以增删改查:

6 示例3:和主表字段一起添加,添加后变成只读不可再新增、更新、删除:

7 示例4: 动态启用/禁用子表

8 示例5:显示为树表(treegrid)

9 示例6:offline模式时显示虚拟字段以及提交时排除虚拟字段

选项:opt={obj, relatedKey, res?, dlg?/关联的明细对话框, datagrid/treegrid}

这些选项在dlg设置时有效:{valueField, readonly, objParam, toolbar, vFields}

1 示例:可以增删改查的子表:

<div class="wui-subobj" data-options="obj:'CusOrder', relatedKey:'cusId', valueField:'orders', dlg:'dlgCusOrder'">
    <p><b>物流订单</b></p>
    <table>
        <thead><tr>
            <th data-options="field:'tm', sortable:true">制单时间</th>
            <th data-options="field:'status', sortable:true, jdEnumMap:CusOrderStatusMap, formatter:Formatter.enum(CusOrderStatusMap), styler:Formatter.enumStyler({CR:'Warning',CL:'Disabled'})">状态</th>
            <th data-options="field:'amount', sortable:true, sorter:numberSort">金额</th>
        </tr></thead>
    </table>
</div>

选项说明:

以下字段仅当关联对话框(即dlg选项设置)后有效:

示例:在对话框dlgOrder上设置子表关联对话框dlgOrder1:

    <div class="wui-subobj" id="tabOrder1" data-options="...">

注意:要在onBeforeShow中设置objParam,如果在onShow中设置就晚了:

jdlg.on("beforeshow", onBeforeShow)
function onBeforeShow(ev, formMode, opt)
{
    var type = opt.objParam && opt.objParam.type;
    var tab1Opt = WUI.getOptions(jdlg.find("#tabOrder1"));
    tab1Opt.objParam = { type: type };
    ...
}

2 动态修改选项

选项可以动态修改,如:

// 在dialog的beforeshow回调中:
var jsub = jdlg.find(".wui-subobj");
WUI.getOptions(jsub).readonly = !g_data.hasRole("emp,mgr");

3 定制子对象操作按钮toobar

示例:只留下删除和刷新:

<div ... class="wui-subobj" data-options="..., toolbar:'rd'"

示例:为子表定制一个操作按钮“取消”:

// function initPageXXX() 自定义个按钮
var btnCancelOrder = {text: "取消订单", iconCls:'icon-delete', handler: function () {
    var row = WUI.getRow(jtbl);
    if (row == null)
        return;
    callSvc("Ordr.cancel", {id: row.id}, function () {
        app_show("操作完成");
        WUI.reloadRow(jtbl, row);
    })
}};

// 在dialog的beforeshow回调中:
var jsub = jdlg.find(".wui-subobj");
WUI.getOptions(jsub).toolbar = ["r", "f", "s", btnCancelOrder]

@see dg_toolbar

4 添加主对象时检查子表

可以在validate事件中,对添加的子表进行判断处理:

function onValidate(ev, mode, oriData, newData) 
{
    if (mode == FormMode.forAdd) {
        // 由于valueField选项设置为"orders", 子表数组会写在newDate.orders中
        if (newData.orders.length == 0) {
            WUI.app_alert("请添加子表项!", "w");
            return false;
        }
        // 假如需要压缩成一个字符串:
        // newData.orders = WUI.objarr2list(newData.orders, ["type", "amount"]);
    }
}

注意:只有在主对象添加时可以检查子表。在更新模式下,子对象的更改是直接单独提交的,主对象中无法处理。

5 示例2:主表记录添加时不需要展示,添加之后子表/关联表可以增删改查:

<div class="wui-subobj" data-options="obj:'CusOrder', relatedKey:'cusId', dlg:'dlgCusOrder'>
    ...
</div>

6 示例3:和主表字段一起添加,添加后变成只读不可再新增、更新、删除:

<div class="wui-subobj" data-options="obj:'CusOrder', relatedKey:'cusId', valueField:'orders', dlg:'dlgCusOrder', readonly: true">
    ...
</div>

示例:最简单的只读子表,只查看,也不关联对话框

<div class="wui-subobj" data-options="obj:'CusOrder', res:'id,tm,status,amount', relatedKey:'cusId'">
</div>

7 示例4: 动态启用/禁用子表

启用或禁用可通过事件发送指令:
jo.trigger("setOption", {disabled: boolDisabledVal}); // 会自动刷新UI

示例: 在物料明细对话框(dlgItem)中, 在Tabs组件中放置"组合"子表, 当下拉框选择"组合"时, 启用"组合"子表Tab页:

<select name="type">
    <option value="">(无)</option>
    <option value="P">组合</option>
    <option value="U">拆卖</option>
</select>

<div class="easyui-tabs">
    <!-- 注意 wui-subobj-item1 类定义了名字为 item1,以便下面setDlgLogic中引用。它一般和valueField相同,但valueField选项可能不存在 -->
    <div class="wui-subobj wui-subobj-item1" data-options="obj:'Item1', valueField:'item1', relatedKey:'itemId', dlg:'dlgItem1'" title="组合">
        ...
    </div>
</div>

设置"组合"页随着type选择启用禁用:

// 注意,subobj组件一般不设置name属性,而是通过定义CSS类`wui-subobj-{name}`类来标识名字,从而可以用 jdlg.gn("item1") 来找到它的通用接口。
WUI.setDlgLogic(jdlg, "item1", {
    watch: "type",
    disabled: function (e) {
        return e.type != "P";
    },
    required: true
});

8 示例5:显示为树表(treegrid)

默认使用以下配置:

{
    idField: "id",  // 不建议修改
    fatherField: "fatherId", // 指向父结点的字段,不建议修改
    treeField: "id", // 显示树结点的字段名,可根据情况修改
}

子表以树表显示时,不支持分页(查询时自动设置参数pagesz=-1)。

@see treegrid

9 示例6:offline模式时显示虚拟字段以及提交时排除虚拟字段

在添加主对象时,对子对象的添加、更新、删除操作不会立即操作数据库,而是将最终子对象列表与主对象一起提交(接口是主对象.add)。
我们称这时的子对象对话框为offline模式,它会带来一个问题,即子对象对话框上点确定后,子表列表中无法显示虚拟字段。

解决方案是:1. 在对话框中用jd_vField选项指定虚拟字段名,2. 在subobj选项中以vFields选项指定这些字段只显示而不最终提交到add接口中。

示例:InvRecord对象包含子表InvRecord1,字段定义为:

@InvRecord1: id, invId, whId, whId2, itemId
vcol: itemName, whName, whName2

打开对话框dlgInvRecord添加对象,再打开子表明细对话框dlgInvRecord1添加子表项。
在subobj组件中,通过选项vFields排除只用于显示而不向后端提交的虚拟字段,dlgInvRecord.html中:

    <div class="wui-subobj" data-options="obj:'InvRecord1', relatedKey:'invId', valueField:'inv1', vFields:'itemName,whName,whName2', dlg:'dlgInvRecord1'" title="物料明细">
        ...子表与字段列表...
    </div>

子表明细对话框中,为了在点击确定后将虚拟字段拷贝回subobj子表列表中,应通过data-options中指定jd_vField选项来指定虚拟字段名,如 dlgInvRecord1.html:

仓库   <select name="whId" class="my-combobox" required data-options="ListOptions.Warehouse()"></select>  (Warehouse函数中已定义{jd_vField: 'whName'})
到仓库 <select name="whId2" class="my-combobox" required data-options="ListOptions.Warehouse({jd_vField:'whName2'})"></select> (覆盖Warehouse函数定义中的jd_vField选项)
物料   <input name="itemId" class="wui-combogrid" required data-options="ListOptions.ItemGrid()">  (ItemGrid函数中已定义{jd_vField: 'itemName'})

ListOptions中对下拉列表参数的设置示例:(store.js)

var ListOptions = {
    ...
    Warehouse: function (opt) {
        return $.extend({
            jd_vField: "whName", // 指定它在明细对话框中对应的虚拟字段名
            textField: "name", // 注意区别于jd_vField,textField是指定显示内容是url返回表中的哪一列
            url: ...
        }, opt);
    },
    ItemGrid: function () {
        return {
            jd_vField: "itemName",
            textField: "name",
            url: ...
        }
    },
}

注意带Grid结尾的选项用于wui-combogrid组件; 否则应用于my-combobox组件;
两者选项接近,wui-combogrid选项中应包含columns定义以指定下拉列表中显示哪些列,而my-combobox往往包含formatter选项来控制显示(默认是显示textField选项指定的列,设置formatter后textField选项无效)

上例中, 通过为组件指定jd_vField选项,实现在offline模式的子表对话框上点确定时,会自动调用WUI.getFormData_vf将虚拟字段和值字段拼到一起,返回并显示到表格中。

@see getFormData_vf

@key easyui-tabs

扩展: 若未指定onSelect回调, 默认行为: 点Tab发出tabSelect事件, 由Tab自行处理

@fn WUI.toggleTab(jtabs, which, show, noEvent?)

禁用或启用easyui-tabs组件的某个Tab页.
which可以是Tab页的索引数或标题.
示例:

var jtabs = jdlg.find(".easyui-tabs");
WUI.toggleTab(jtabs, "组合物料", formData.type == "P");

@key .wui-picker 字段编辑框中的小按钮

@key .wui-picker-edit 手工编辑按钮

示例:输入框后添加一个编辑按钮,默认不可编辑,点按钮编辑:

<input name="value" class="wui-picker-edit">

特别地:

要查看该字段是否绝对只读(不显示picker按钮,不可手工编辑),可以通过gn函数:

var it = jo.gn(); // 或从对话框来取如 jdlg.gn("xxx")
var ro = it.readonly();
it.readonly(ro);

@key .wui-picker-help 帮助按钮

示例:输入框后添加一个帮助按钮:

<input name="value" class="wui-picker-help" data-helpKey="取消工单">

点击帮助按钮,跳往WUI.options.helpUrl指定的地址。如果指定data-helpKey,则跳到该锚点处。

可以多个picker一起使用。

帮助链接:(加wui-help类则点击可跳转,同时也支持用data-helpKey属性指定主题)

<a class="wui-help"><span><i class="fa fa-question-circle"></i>帮助</span></a>

@key .wui-more 可折叠的在线帮助信息

显示一个按钮,用于隐藏(默认)或显示后面的内容。基于easyui-linkbutton创建,兼容该组件的options比如图标.

示例: 列对应: title=code,-,amount 
<span class="wui-more" data-options="iconCls: 'icon-tip'">更多示例</span>
<pre>
映射方式对应: title=编码->code, Total Sum->amount&amp;useColMap=1
根据code, 存在则更新: title=code,amount&amp;uniKey=code
根据code, 批量更新: title=code,amount&amp;uniKey=code!
带子表: title=code,amount,@order1.itemCode,@order1.qty&amp;uniKey=code
</pre>

<span class="wui-more"><i class="fa fa-question-circle"></i> 代码示例</span>
<pre class="hint">
$env->get("地址", "value");
$env->set("地址", "value", "上海市XX区");
</pre>

@see .wui-help

@fn WUI.showByType(jo, type)

(v6) 该函数已不建议使用。本来用于显示多组mycombobox/wui-combogrid组件,现成推荐直接用组件的setOption事件动态修改组件选项。

对话框上form内的一组控件中,根据type决定当前显示/启用哪一个控件。

需求:ItemStatusList定义了Item的状态,但当Item类型为“报修”时,其状态使用ItemStatusList_报修,当类型为“公告”时,状态使用ItemStatusList_公告,其它类型的状态使用ItemStatusList

HTML: 在对话框中,将这几种情况都定义出来:

<select name="status" class="my-combobox" data-options="jdEnumList:ItemStatusList"></select>
<select name="status" class="my-combobox type-报修" style="display:none" data-options="jdEnumList:ItemStatusList_报修"></select>
<select name="status" class="my-combobox type-公告" style="display:none" data-options="jdEnumList:ItemStatusList_公告"></select>

注意:当type与“报修”时,它按class为"type-报修"来匹配,显示匹配到的控件(并添加active类),隐藏并禁用其它控件。
如果都不匹配,这时看第一条,如果它不带type-xxx类,则使用第一条来显示,否则所有都不启用(隐藏、禁用)。

JS: 根据type设置合适的status下拉列表,当type变化时更新列表:

function initDlgXXX()
{
    ...
    jdlg.on("beforeshow", onBeforeShow);
    // 1. 根据type动态显示status
    $(frm.type).on("change", function () {
        var type = $(this).val();
        WUI.showByType(jfrm.find("[name=status]"), type);
    });
    function onBeforeShow(ev, formMode, opt) 
    {
        ...
        setTimeout(onShow);
        function onShow() {
            ...
            // 2. 打开对话框时根据type动态显示status
            $(frm.type).trigger("change");
        }
    }
}

支持combogrid组件,注意combogrid组件用"[comboname]"而非"[name]"来找jQuery组件:

WUI.showByType(jdlg.find("[comboname=orderId]"), type);

HTML示例:

<tr>
    <td class="orderIdTd">关联单据</td>
    <td>
        <input name="orderId" class="wui-combogrid type-生产领料 type-生产调拨 type-生产入库 type-生产退料" data-options="ListOptions.OrderGrid({type:'生产工单'})">
        <input name="orderId" class="wui-combogrid type-销售" data-options="ListOptions.OrderGrid({type:'销售计划'})">
        ...
    </td>
</tr>

支持组件状态被动态修改,比如添加模式时打开对话框就禁用该组件:

jdlg.find("[comboname=orderId]").combogrid({disabled: forAdd});

这时调用showByType后,active组件仍会保持disabled状态。类似的,如果调用者先隐藏了组件,则调用showByType后active组件也是隐藏的。

注意:对tr等包含输入框的块组件也可使用,但要求内部只有一个带name的输入组件,且各块的内部输入组件的name都相同。

    <tr class="optional type-出库">...<input name="orderId">...</tr>
    <tr class="optional type-入库">...<input name="orderId">...</tr>

JS控制:

WUI.showByType(jdlg.find("tr.optional"), type);

当一个块不显示时,其内部的带name的输入组件被设置disabled,提交时不会带该字段。
如果块内部包含多个输入框,或各块内的输入框的name不同,如果各块内所有输入框都默认显示、未禁用(也不会动态修改显示、禁用),这时也可以使用showByType,否则会有问题。

@fn diffObj(obj, obj1)

返回null-无差异,如果是对象与对象比较,返回差异对象,否则返回true表示有差异

var rv = diffObj({a:1, b:99}, {a:1, b:98, c:100});
// rv: {b:98, c:100}

var rv = diffObj([{a:1, b:99}], [{a:1, b:99}]);
// rv: null (无差异)

var rv = diffObj([{a:1, b:99}], [{a:1, b:99}, {a:2}]);
// rv: true

var rv = diffObj("hello", "hello");
// rv: null (无差异)

var rv = diffObj("hello", 99);
// rv: true

@fn showDataReport(opt={ac, @gres, @gres2?, res?, cond?, title?="统计报表", detailPageName?, detailPageParamArr?, resFields?, cond2?, tmField?, showChart?, chartType?=line})

选项说明:

@see JdcloudStat.tmUnit

示例:

WUI.showDataReport({
    ac: "Employee.query",
    gres: ["depName 部门", "性别"],
    gres2: ["职称", "学历"],
    cond: WUI.getQueryCond({status: '在职'}),
    detailPageName: "pageEmployee1",
    title: "师资报表"
});

示例:订单月报

WUI.showDataReport({
    ac: "Ordr.query",
    res: "SUM(amount) 总和", // 不指定则默认为`COUNT(1) 总数`
    tmField: "createTm 创建时间",
    gres: ["y 年,m 月", "status 状态=CR:新创建;RE:已完成;CA:已取消"],
    gres2: ["dscr 订单类别"],
    cond: WUI.getQueryCond({createTm: ">=2020-1-1 and <2021-1-1", status: "CR,RE,CA"}), // 生成条件字符串
    detailPageName: "pageOrder",
    title: "订单月报",

    // 定义用户可选的字段,定义它或gres/gres2会在工具栏显示“统计”按钮。注意不需要定义y,m等时间字段,它们由tmField自动生成。
    resFields: "amount 金额, status 状态=CR:新创建;RE:已完成;CA:已取消, dscr 订单类别, userName 用户, userPhone 用户手机号, createTm 创建时间",

    // showChart: true, // 显示统计图
    // gres: ["y 年,m 月"],
    // tmUnit: "y,m",
});

示例:订单状态占比, 显示饼图(当只有gres没有gres2,且没有tmUnit时,可自动显示饼图)

WUI.showDataReport({
    ac: "Ordr.query",
    res: "COUNT(1) 总数", // 不指定则默认为`COUNT(1) 总数`
    gres: ["status 状态=CR:新创建;RE:已完成;CA:已取消"],
    detailPageName: "pageOrder",
    title: "订单状态占比",

    // 定义用户可选的字段,定义它或gres/gres2会在工具栏显示“统计”按钮。注意不需要定义y,m等时间字段,它们由tmField自动生成。
    resFields: "amount 金额, status 状态=CR:新创建;RE:已完成;CA:已取消, dscr 订单类别, userName 用户, userPhone 用户手机号, createTm 创建时间",

    showChart: true, // 显示统计图
    orderby: "总数 DESC"
});

示例:多个统计项:订单状态报表,同时统计数量和金额:

WUI.showDataReport({
    ac: "Ordr.query",
    res: "COUNT(1) 总数, SUM(amount) 总金额",
    gres: ["status 状态=CR:新创建;RE:已完成;CA:已取消"],
    // gres2: ["dscr 订单类别"], // 试试加上列统计项有何样式区别
    detailPageName: "pageOrder",
    title: "订单状态占比",
    // showSum: true, // 自动添加行列统计

    // 定义用户可选的字段,定义它会在工具栏显示“统计”按钮。注意不需要定义y,m等时间字段,它们由tmField自动生成。
    resFields: "amount 金额, status 状态=CR:新创建;RE:已完成;CA:已取消, dscr 订单类别, userName 用户, userPhone 用户手机号, createTm 创建时间",
});

注意:统计图(showChart:true)目前只支持第一个统计项。

res中也可以加一些不用于统计的字段,统计列必须放在res最后面,系统会自动将res中最后若干个连续地以COUNT或SUM聚合的字段会当成统计列。

WUI.showDataReport({
    ac: "Ordr.query",
    res: "userName 用户, userPhone 手机号, createTm 创建时间, COUNT(1) 总数, SUM(amount) 总金额", // 自动识别2个统计列,调用query接口时设置参数`pivotCnt:2`
    gres: ["status 状态=CR:新创建;RE:已完成;CA:已取消"],
    gres2: ["dscr 订单类别"],
    detailPageName: "pageOrder",
    title: "订单状态占比",
    showSum: true, // 自动添加行列统计
});

@fn showDlgChart(data, rs2StatOpt, seriesOpt, chartOpt)

@see WUI.rs2Stat 图表数据转换

@see WUI.initChart 显示图表

@see pageSimple 通用列表页

示例:

// 各状态订单数: 柱图
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "status =CR:新创建;RE:已完成;CA:已取消",
    res: "count(*) 总数",
}));

// 也可以与pageSimple列表页结合,同时显示列表页和统计图:
var url = WUI.makeUrl("Ordr.query", {
    gres: "status 状态=CR:新创建;RE:已完成;CA:已取消",
    res: "count(*) 总数",
});
var showChartParam = []; // 必须指定数组,即showDlgChart的后三个参数:[rs2StatOpt, seriesOpt, chartOpt]
WUI.showPage("pageSimple", "订单统计!", [url, null, null, showChartParam]);

// 各状态订单数: 饼图,习惯上应占比排序
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "status =CR:新创建;RE:已完成;CA:已取消",
    res: "count(*) 总数",
    orderby: "总数 DESC"
}), null, {
    type: "pie"
});

// 订单年报
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y",
    res: "count(*) 总数",
}));

// 订单月报,横坐标为年月(两列)
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y,m",
    res: "count(*) 总数",
}), {  // rs2StatOpt
    xcol:[0,1]
});

// 订单月报,指定tmUnit,注意加orderby让时间排序,支持自动补齐缺少的时间
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y,m",
    res: "count(*) 总数",
    orderby: "y,m"
}), {  // rs2StatOpt
    tmUnit: "y,m"
});

// 也可以与pageSimple列表页结合,同时显示列表页和统计图:
var url = WUI.makeUrl("Ordr.query", {
    gres: "y 年,m 月",
    res: "count(*) 总数",
    orderby: "y,m"
});
var showChartParam = [ {tmUnit: "y,m"} ];
WUI.showPage("pageSimple", "订单统计!", [url, null, null, showChartParam]);

// 订单各月占比,显示为饼图,哪个月订单多则排在前
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y,m",
    res: "count(*) 总数",
    orderby: "总数 DESC"
}), {  // rs2StatOpt
    xcol:[0,1]
}, { // seriesOpt
    type: "pie"
}, { // chartOpt
    title: { text: "订单各月分布" }
});

// 分状态订单月报
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y,m,status =CR:新创建;RE:已完成;CA:已取消",
    res: "count(*) 总数",
    orderby: "y,m"
}), {  // rs2StatOpt
    tmUnit: "y,m"
});

// 同上,配置堆积柱状图,配置stack
WUI.showDlgChart(callSvr("Ordr.query", {
    gres: "y,m,status =CR:新创建;RE:已完成;CA:已取消",
    res: "count(*) 总数",
    orderby: "y,m"
}), {  // rs2StatOpt
    tmUnit: "y,m"
}, { // seriesOpt
    type: "bar",
    stack: "X"
}, { // chartOpt
    // swapXY: true // 横向柱状图
});

// 分用户订单月报
WUI.showDlgChart(callSvr("Ordr.query", {
    cond: {createTm: ">=2010-1-1 and <2030-1-1"},
    gres: "userId",
    res: "userName,count(*) 总数",
    orderby: "总数 DESC"
}), {  // rs2StatOpt
    xcol: 1
},{ // seriesOpt
    type: 'pie', // 饼图
});

// 分用户订单月报
WUI.showDlgChart(callSvr("Ordr.query", {
    cond: {createTm: ">=2020-1-1 and <2021-1-1"},
    gres: "y,m,userId",
    res: "userName,count(*) 总数",
    orderby: "y,m"
}), {  // rs2StatOpt
    tmUnit: "y,m"
}, { // seriesOpt
    type: 'bar',
});

@module ObjLog 操作日志

系统默认会记录操作日志ObjLog,可在管理端展示:

@fn setDlgLogic(jdlg, name, logic)

1 readonly-是否只读,disabled-是否禁用,show-是否显示,value-设置值

2 watch-监控变化

2.1 从下拉列表中取值

2.2 使用onWatch选项

3 动态设置combogrid/subobj等组件选项

4 提交前验证

@key .wui-dialog-logic 指定对话框逻辑

设置对话框业务逻辑。

1 readonly-是否只读,disabled-是否禁用,show-是否显示,value-设置值

示例:对orderId组件,添加之后不可以修改:

// 一般可定义在initDlgXXX函数中
WUI.setDlgLogic(jdlg, "orderId", {
    readonlyForSet: true
});

也可以定义在DOM对象上,如:

<input name="orderId" class="wui-dialog-logic" data-options="readonlyForSet:true">

如果是叠加在wui-combogrid组件上:

<input name="orderId" class="wui-combogrid wui-dialog-logic" data-options="$.extend(ListOptions.UserGrid(), readonlyForSet:true)">

logic中支持readonly选项,也支持readonlyForAdd/readonlyForSet这些特定模式的选项,它们既可以设置为一个值,也可以是一个函数fn(data, gn)(或等价的lambda表达式):

如果同时指定了readonly和readonlyForAdd选项,则当添加模式时优先用readonlyForAdd选项,其它模式也类似。

这个特性同样适用于以下disabled/readonly/value选项,比如有disabledForAdd/readonlyForSet/valueForAdd等选项。

示例:添加时无须填写,但之后可修改:可设置添加时隐藏或只读

showForAdd: false
或
readonlyForAdd: true

注意:

示例:添加时自动填写

填当前日期:直接设置值
valueForAdd: new Date().format("D")

或指定lambda或函数:
valueForAdd: () => new Date().format("D")
(与直接指定值的区别是每次打开对话框时都会计算)

填当前操作员:
valueForAdd: () => g_data.userInfo.id

如果不允许改,则加上

readonlyForAdd: true

示例:计算字段(不提交)

disabled: true
value: e => e.x + "-" + e.y

由于后端没这个实体字段,所以不能提交。注意readonly和disabled在前端效果相似,但readonly字段会提交而disabled字段不提交。

2 watch-监控变化

示例:根据status值,如果为CR就显示orderId字段

WUI.setDlgLogic(jdlg, "orderId", {
    show: e => e.status == "CR",
    watch: "status" // 指定依赖字段,当它变化时刷新当前字段
});

上面用的是lambda写法,参数e是对话框初始数据。
如果浏览器不支持lambda,可以用函数形式:

WUI.setDlgLogic(jdlg, "orderId", {
    show: function (e) {
        return e.status == "CR";
    },
    watch: "status"
});

类似于readonly选项,选项show也支持showForAdd, showForSet, showForFind这些扩展选项。

上面如果只设置show选项,则只会在打开对话框时设置字段,加上watch选项指定依赖字段,则当依赖字段发生变化时也会动态刷新。
甚至可以依赖多字段,用逗号隔开即可,如单价(price)或数量(qty)变化后,更新总价(total):

WUI.setDlgLogic(jdlg, "total", {
    value: e => e.price * e.qty,
    watch: "price,qty" // 依赖多字段,以逗号分隔,注意不要多加空格
});

watch选项会刷新disabled/readonly/show/value系列选项中的表达式。

2.1 从下拉列表中取值

示例:商品字段(itemId)使用下拉数据表(wui-combogrid下拉框组件)显示,有id,name,price三列,如果从下拉列表中选择了一个商品,则自动用选中行的price列来更新对话框上的price字段:

WUI.setDlgLogic(jdlg, "price", {
    value: e => e.eventData_itemId?.price,
    watch: "itemId"
});

当选择商品后,可以从e.eventData_itemId数据中取到下拉列表的值(其中itemId是下拉列表对应的字段名),如e.eventData_itemId.price
e.eventData_itemId?.price中的?.是ES6写法,相当于e.eventData_itemId && e.eventData_itemId.price(如果e.eventData_itemId有值时再取e.eventData_itemId.price,避免字段为空报错)。

类似地,如果是其它字段变化,则可以从e.eventData_{字段名}中取值。my-combobox组件下拉时也是返回选择行对应的数据。

注意:如果有其它字段依赖了修改的值,比如上节例子中total字段依赖price,则此时会连锁地去更新total字段。

2.2 使用onWatch选项

更自由地,可以用onWatch选项定义一个函数,当watch字段变化后执行,原型为:onWatch(e=当前对象数据, ev=扩展数据, gn=字段访问器),在ev参数中:

参数gn是一个函数,可以对任意字段进行处理,用gn(字段名)可以取到该字段,然后可以链式调用通用接口,如gn("orderId").visible(true).disabled(false).readonly(true).val(100)

示例:当选择了一个工件(snId, 使用combogrid组件)后,自动填充工单(orderId, 使用combogrid组件)、工单开工时间(actualTm)等字段。

WUI.setDlgLogic(jdlg, "snId", {
    watch: "snId",
    onWatch: async function (e, ev, gn) {
        console.log(ev);
        // combogrid组件设置值可以用一个数组,同时设置value和text
        gn("orderId").val([ev.data.orderId, ev.data.orderCode]);

        // ev.data中没有现成数据,故再调用接口查一下
        var rv = await callSvr("Ordr.get", {id: ev.data.orderId, res: "actualTm"})
        gn("actualTm").val(rv.actualTm);
    }
});

@see jQuery.fn.gn

3 动态设置combogrid/subobj等组件选项

可使用setOption(e, it, gn)来设置组件。

示例:type="入库"时,下拉列表moveType字段显示入库选项,type="出库"时,显示出库选项

var MoveTypeMap_出库 = {
    602: "销售出库冲销",
    202: "成本中心",
    222: "项目",
    552: "报废冲销"
};

var MoveTypeMap_入库 = {
    102: "生产入库冲销",
    201: "成本中心领料"
}

// html中的组件: <input name="moveType" class="my-combobox">
WUI.setDlgLogic(jdlg, "moveType", {
    setOption: function (e, it, gn) {
        return {
            jdEnumMap: e.type == "入库"? MoveTypeMap_入库: MoveTypeMap_出库
        }
    },
    watch: "type"
});

也可以直接设置静态值:

// html中:<input class="wui-combogrid">
WUI.setDlgLogic(jdlg, "whId", {
    setOption: ListOptions.WhGrid()
});

这与直接在html中设置是等价的:

<input class="wui-combogrid" data-options="ListOptions.WhGrid()">

4 提交前验证

required: true,

// 返回一个非空字符串,则表示验证失败,字符串即是错误信息
validate: v => /^\d{11}$/.test(v) || "手机号须11位数字"

注意验证选项对添加、更新有效,没有forAdd/forSet选项。

validate函数原型为:validate(value, it, gn)

示例:当status字段值为RE时,当前字段值不可为空:

{
    validate: (v,it,gn) => {
        if (gn("status").val() == "RE" && !v)
            return "单据完成时[" + it.getTitle() + "]不可为空";
    }
}

用gn取其它字段值;用it.getTitle()取当前字段标题。

注意:当字段未在对话框中显示,或是禁用状态时,或是只读状态时,不执行验证。

子表(wui-subobj)对象一样支持required/validate,在validate函数中传入值v是一个数组,如果没有填写则v.length为0。示例:

{
    //required: true,
    validate: function (value, it, gn) {
        if (value.length < 2) {
            return "明细表至少添加2行!";
        }
    }
}

此外,还可以设置validType选项,它与easyui-validatebox组件兼容。示例:

{
    required: true,
    validType: "email"
}

注意:validType与validate选项不可一起使用。

@see .easyui-validatebox

@module JdcloudStat

按日期进行数据分析统计

@see initPageStat

@var WUI.options.statFormatter

@key JdcloudStat.tmUnit 按时间维度分析

tmUnit指定时间维度分析的类型,目前支持以下维度:

"y,m"     年月
"y,m,d"   年月日
"y,m,d,h" 年月日时
"y,w"     年周
"y,q"     年季度

@fn WUI.pivot(rs, opt)

@param opt {gcol, xcol, ycol?, gtext?, maxSeriesCnt?, formatter?}

gcol/gtext, xcol, ycol可以是数字, 表示第几列; 也可以是数字数组, 表示若干列.
gtext也是列号或列号数组,与gcol合用,表示按gcol分组,最终“组名”结果按gtext列显示。
如果指定formatter,则“组名”再经formatter处理后显示。formatter(val): val是一个值或一个数组,由gcol/gtext决定。

如果指定opt.maxSeriesCnt,则分组最多maxSeriesCnt列,其它组则都归并到“其它”组。

示例: 按年-月统计各产品类别(cateId)的订单金额, 可以调用接口:

callSvr("Ordr.query", {gres: "y,m,cateId", res: "cateName,SUM(amount) sum"}, orderby: "y,m")

得到rs表格:

var rs = {
    h: ["y","m","cateId","cateName","sum"],
    d: [
        [2019, 11, 1, "衣服", 20000],
        [2019, 11, 2, "食品", 12000],
        [2019, 12, 2, "食品", 15000],
        [2020, 02, 1, "衣服", 19000]
    ]
}

即:

y   m   cateId  cateName    sum
------------
2019    11  1   衣服  20000
2019    11  2   食品  12000
2019    12  2   食品  15000
2020    02  1   衣服  19000
  1. 将分类cateId转到列上(但按cateName来显示):

    var rs1 = pivot(rs, {
    xcol: [0, 1], // 0,1列
    gcol: 2, // 按该列转置
    gtext: 3, // 表示第3列是第2列的显示内容, 也可以用函数function (row) { return row[3] }
    ycol: 4, // 值列, 可以不指定, 缺省为最后一列.
    })

得到结果:

rs1 = {
    h: ["y","m","衣服","食品"],
    d: [
        [2019, 11, 20000, 12000],
        [2019, 12, null, 15000],
        [2020, 02, 19000, null]
    ]
}

即:

y   m 衣服    食品
------------
2019    11  20000   12000
2019    12  null    15000
2020    02  19000   null
  1. 若xcol中只保留年:

    var rs1 = pivot(rs, {
    xcol: 0,
    gcol: 2,
    gtext: 3,
    ycol:4
    })

得到结果将变成这样:

rs1 = {
    h: ["y","衣服","食品"],
    d: [
        [2019, 20000, 27000],
        [2020, 19000, null]
    ]
}
  1. 若在xcol中保留分类, 将年-月转到列上:

    var rs1 = pivot(rs, {
    xcol: [2, 3], // 或只留3也可以
    gcol: [0, 1],
    ycol:4
    })

得到结果:

rs1 = {
    h: ["cateId","cateName","2019-11","2019-12","2020-2"],
    d: [
        [1, "衣服", 20000, null, 19000],
        [2, "食品", 12000, 15000, null]
    ]
}

即:

cateId cateName 2019-11 2019-12 2020-02
------------
1   衣服  20000   0   19000
2   食品  12000   15000   0
  1. ycol也可以指定多列, 用的比较少.

    var rs1 = pivot(rs, {
    xcol: [0, 1],
    gcol: 3,
    ycol: [4,4], // 为演示结果, 故意重复sum列. 实际可能为"订单总额","订单数"两列.
    })

得到结果:

rs1 = {
    h: ["y","m","衣服","食品"],
    d: [
        [2019, 11, [20000,20000], [12000,12000]],
        [2019, 12, null, [15000,15000]],
        [2020, 02, [19000,19000], null]
    ]
}

@fn WUI.rs2Stat(rs, opt?) -> statData

1 常用统计图示例

2 tmUnit使用举例

将query接口返回的数据,转成统计图需要的数据格式。

@param opt {xcol, ycol, gcol, gtext, maxSeriesCnt, tmUnit, formatter, formatterX, noTmInsert=0}

@param opt.xcol 指定X轴数据,可以是一列或多列,如0表示第0列, 值[0,1]表示前2列。可以没有x列,用空数组`[]`表示。

@param opt.ycol 指定值数据,可以是一列或多列。

@param opt.gcol 指定分组列。

@param opt.gtext 指定分组值对应的显示文本列。比如表中既有商品编号,又有商品名称,商品编号列设置为gcol用于分组,而商品名称列设置为gtext用于显示。

xcol,ycol,gcol,gtext,maxSeriesCnt参数可参考函数 WUI.pivot

@param opt.tmUnit 如果非空,表示按指定时间维度分析。参考[JdcloudStat.tmUnit]().

未指定tmUnit时,缺省xcol=0列(或1列,当有3列及以上且未指定ycol时,当成是xcol,xcoltext,ycol三列格式),ycol=最后一列,gcol如需要则应手工指定

tmUnit用于指定时间字段: "y,m"-年,月; "y,m,d"-年,月,日; "y,w"-年,周; "y,m,d,h"-年,月,日,时; "y,q"-年,季度
若指定了tmUnit,则可以不指定xcol,gcol,ycol,而是由字段排列自动得到,详见"tmUnit使用举例"章节。

@param opt.formatter 对汇总数据列进行格式化,缺省取WUI.options.statFormatter[ycolNames]。Function(value).

@param opt.formatterX 对X轴数据进行格式化,缺省取WUI.options.statFormatter[xcolNames]。Function(value)。若opt.xcol是数组,则value也是数组。

@return statData { @xData, @yData=[{name=seriesName, data=@seriesData}] }

与echart结合使用示例可参考 initChart. 原理如下:

var option = {
    ...
    legend: {
        data: statData.yData
    },
    xAxis:  {
        type: 'category',
        boundaryGap: false,
        data: statData.xData
    },
    yAxis: {
        type: 'value',
        axisLabel: {
            formatter: '{value}'
        }
    },
    series: statData.yData
};
myChart.setOption(option);

1 常用统计图示例

例1:统计每个用户的订单金额, 由高到低显示前10名, 显示为饼图或柱状图.

callSvr("Ordr.query", {gres: "userId 用户编号", res: "userName 用户, SUM(amount) 金额", orderby: "sum DESC", pagesz: 10})
// 一般用userId而不是userName来分组, 因为不仅userName可能会重名, 而且userName一般是从外部表join过来的, 没有索引性能较差不适合做分组字段.

得到结果示例:

var rs = {
    h: ["用户编号", "用户", "金额"],
    d: [
        [1001,"用户1",12000],
        [1002,"用户2",10000]
    ]
}

即:

用户编号    用户  金额
-------------
1001    用户1 12000
1002    用户2 10000
...

通过rs2Stat转换:

var statData = rs2Stat(rs, {xcol:1}); // xcol指定横轴数据列, 缺省为第0列, 这里指定为第1列,用名字替代编号。ycol选项可指定统计值列, 这里没有指定,缺省为最后一列,
// 结果:
statData = {
    xData: [
        '用户1', '用户2'
    ],
    yData: [
        {name: '金额', data: [12000, 10000]}
    ]
}

例2:按年-月统计订单金额, 显示为柱状图或折线图.

callSvr("Ordr.query", {gres: "y,m", res: "SUM(amount) sum", orderby: "y,m"})

得到表:

y   m   sum
-----------
2019    11  30000
2019    12  34000
2020    2   25000

转换示例:

var rs = {
    h: ["y", "m", "sum"],
    d: [
        [2019,11,30000],
        [2019,12,34000],
        [2020,2,25000],
    ]
}
var statData = rs2Stat(rs, {xcol:[0,1]});
// 结果:
statData = {
    xData: [
        '2019-11', '2019-12', '2020-2'
    ],
    yData: [
        {name: '累计', data: [30000, 34000, 25000]}
    ]
}

上面年月中缺少了2020-1, 如果要补上缺少的月份, 可以使用tmUnit参数指定日期类型, 注意这时原始数据中年月须已排好序:

var statData = rs2Stat(rs, {xcol:[0,1], ycol:2, tmUnit:"y,m"} );
// 指定tmUnit后, xcol缺省为前N列即tmUnit这几列, 如"y,m,d"(年月日)表示前3列即`xcol: [0,1,2]`. 上面参数可简写为:
var statData = rs2Stat(rs, {tmUnit:"y,m"} );
// 结果:
statData = {
    xData: [
        '2019-11', '2019-12', '2020-1', '2020-2'
    ],
    yData: [
        {name: '累计', data: [30000, 34000, 0, 25000]}
    ]
}

例3:按年-月统计各产品类别(cateId)的订单金额, 产品类别在列上显示(即显示为系列, 列为"年,月,类别1,类别2,..."):

callSvr("Ordr.query", {gres: "y,m,cateId", res: "cateName,SUM(amount) sum"}, orderby: "y,m")

y   m   cateId  cateName    sum
------------
2019    11  1   衣服  20000
2019    11  2   食品  12000
2019    12  2   食品  15000
2020    02  1   衣服  19000

结果需要将分类cateName转到列上, 即:

y   m 衣服    食品
------------
2019    11  20000   12000
2019    12  0   15000
2020    02  19000   0

可以添加gcol参数指定转置列(pivot column):

var rs = {
    h: ["y","m","cateId","cateName","sum"],
    d: [
        [2019, 11, 1, "衣服", 20000],
        [2019, 11, 2, "食品", 12000],
        [2019, 12, 2, "食品", 15000],
        [2020, 02, 1, "衣服", 19000]
    ]
}
var statData = rs2Stat(rs, {
    xcol: [0, 1], // 0,1列
    gcol: 2,
    gtext: 3 // gtext表示gcol如何显示, 数字3表示按第3列显示, 即"1","2"显示成"衣服", "食品"; gtext也可以用函数, 如 `function (val, row, i) { return row[3] }`
})
// 结果:
statData = {
    xData: [
        '2019-11', '2019-12', '2020-2'
    ],
    yData: [
        {name: '衣服', data: [20000, 0, 19000]},
        {name: '食品', data: [12000, 15000, 0]}
    ]
}

如果还需要补上缺少的年月, 可以加tmUnit参数, 要求原始数据中年月须已排好序:

var statData = rs2Stat(rs, {
    // xcol: [0, 1], // 有tmUnit参数时, 且刚好前N列表示时间, 则xcol可缺省
    gcol: 2,
    gtext: 3,
    tmUnit: "y,m"
})
// 结果:
statData = {
    xData: [
        '2019-11', '2019-12', '2020-1', '2020-2' // '2020-1'是自动补上的
    ],
    yData: [
        {name: '衣服', data: [20000, 0, 0, 19000]},
        {name: '食品', data: [12000, 15000, 0, 0]}
    ]
}

注意: 上面将分类cateName转到列上再转成统计数据, 也可以分步来做, 先用pivot函数:

var rs1 = pivot(rs, {
    xcol: [0, 1],
    gcol: 2,
    gtext: 3
})

得到结果rs1:

y   m 衣服    食品
------------
2019    11  20000   12000
2019    12  0   15000
2020    02  19000   0

再将rs1转成统计数据:

var statData = rs2Stat(rs1, {
    xcol: [0, 1],
    ycol: [2, 3], // 注意这时ycol是多列, 显式指定.
    tmUnit: "y,m"
})

2 tmUnit使用举例

当有tmUnit时, 列按如下规则分布, 可以省去指定xcol, gcol等参数:

与tmUnit匹配的时间列   值统计列
与tmUnit匹配的时间列   分组列 值统计列
与tmUnit匹配的时间列   分组列 组名列 值统计列

例如以下列, 均可以只用参数 {tmUnit: "y,m,d"}:

y,m,d,sum  完整参数为: { tmUnit: "y,m,d", xcol:[0,1,2], ycol: 3 }
y,m,d,cateName,sum  完整参数为: { tmUnit: "y,m,d", xcol:[0,1,2], gcol:3, ycol: 4 }
y,m,d,cateId,cateName,sum  完整参数为:   { tmUnit: "y,m,d", xcol:[0,1,2], gcol:3, gtext:4, ycol: 5 }

指定tmUnit后会自动补齐缺失日期,并按预定格式显示。如果不想补日期,可以设置noTmInsert:1选项。

示例一:

var rs = {
    h: ["y", "m", "d", "sum"], // 时间维度为 y,m,d; sum为统计值
    d: [
        [2016, 6, 29, 13],
        [2016, 7, 1, 2],
        [2016, 7, 2, 9],
    ]
}
var statData = rs2Stat(rs, {tmUnit: "y,m,d"});

// 结果:
statData = {
    xData: [
        '2016-6-29', '2016-6-30', '2016-7-1', '2016-7-2' // 2016-6-30为自动补上的日期
    ],
    yData: [
        {name: 'sum', data: [13, 0, 2, 9]} // 分别对应xData中每个日期,其中'2016-6-30'没有数据自动补0
    ]
}

示例二: 有汇总字段

var rs = {
    h: ["y", "m", "d", "sex", "sum"], // 时间维度为 y,m,d; sex为汇总字段, sum为累计值
    d: [
        [2016, 6, 29, '男', 10],
        [2016, 6, 29, '女', 3],
        [2016, 7, 1, '男', 2],
        [2016, 7, 2, '男', 8],
        [2016, 7, 2, '女', 1],
    ]
}
var statData = rs2Stat(rs, {tmUnit: "y,m,d"});

// 结果:
statData = {
    xData: [
        '2016-6-29', '2016-6-30', '2016-7-1', '2016-7-2' // 2016-6-30为自动补上的日期
    ],
    yData: [
        {name: '男', data: [10, 0, 2, 8]}, // 分别对应xData中每个日期,其中'2016-6-30'没有数据自动补0
        {name: '女', data: [3, 0, 0, 1]} // '2016-6-30'与'2016-7-1'没有数据自动补0.
    ]
}

默认yData中的系列名(seriesName)直接使用汇总字段,但如果汇总字段后还有一列,则以该列作为显示名称。

示例三: 汇总字段"sex"后面还有一列"sexName", 因而使用sexName作为图表系列名用于显示. 而"sex"以"M","F"分别表示男女,仅做内部使用:

var rs = {
    h: ["y", "m", "d", "sex", "sexName", "sum"], // 时间维度为 y,m,d; sex为汇总字段, sexName为汇总显示字段, sum为累计值
    d: [
        [2016, 6, 29, 'M', '男', 10],
        [2016, 6, 29, 'F', '女', 3],
        [2016, 7, 1, 'M', '男', 2],
        [2016, 7, 2, 'M', '男', 8],
        [2016, 7, 2, 'F', '女', 1],
    ]
}
var statData = rs2Stat(rs, {tmUnit: "y,m,d"});
// 结果:与示例二相同。它等价于调用:
var statData = rs2Stat(rs, {tmUnit: "y,m,d",
    xcol: [0,1,2],
    gcol: 3,
    gtext: 4,
    ycol: 5
});

示例四: 汇总字段支持格式化,假设性别字段以'M','F'分别表示'男', '女':

var rs = {
    h: ["y", "m", "d", "sex", "sum"], // 时间维度为 y,m,d; sex为汇总字段, sum为累计值
    d: [
        [2016, 6, 29, 'M', 10],
        [2016, 6, 29, 'F', 3],
        [2016, 7, 1, 'M', 2],
        [2016, 7, 2, 'M', 8],
        [2016, 7, 2, 'F', 1],
    ]
}
var opt = {
    tmUnit: "y,m,d",
    // arr为当前行数组, i为统计字段在数组中的index, 即 arr[i] = value.
    formatter: function (value, arr, i) {
        return value=='M'?'男':'女';
    }
};
var statData = rs2Stat(rs, opt);
// 结果:与示例二相同。

可以使用变量WUI.options.statFormatter指定全局formatter,如示例四也可以写:

WUI.options.statFormatter = {
    sex: function (value, arr, i) {
        return value=='M'?'男':'女';
    }
}
var statData = rs2Stat(rs, {tmUnit: "y,m,d"});

在无汇总时,列"sum"会自动被改为"累计",这时默认在statFormatter中设置的:

WUI.options.statFormatter = {
    sum: function (value) {
        return '累计';
    }
}

X轴数据也支持定制,通过设置formaterX回调函数,如tm="y,m,d"时,默认显示如"2020-10-1",现在想只显示"10-1",可以用:

var statData = WUI.rs2Stat(rs, {tmUnit:"y,m,d", formatterX:function (value) {
    return value[1] + "-" + value[2]
}})

@fn WUI.initChart(chartTable, statData, seriesOpt, chartOpt)

初始化echarts报表组件.

statData示例:

statData = {
    xData: [
        '2016-6-29', '2016-6-30', '2016-7-1', '2016-7-2'
    ],
    yData: [
        {name: 'sum', data: [13, 0, 2, 9]} // 分别对应xData中每个日期,其中'2016-6-30'没有数据自动补0
    ]
}

特别地,设置 chartOpt.swapXY = true,表示横向柱状图。

@see WUI.rs2Stat WUI.initPageStat

@fn WUI.initPageStat(jpage, setStatOpt) -> statItf

通用统计页模式

示例可参考超级管理端API日志统计(web/adm/pageApiLogStat)

html示例:

<div wui-script="pageUserRegStat.js" title="用户注册统计" my-initfn="initPageUserRegStat">
    开始时间 
    <input type="text" name="createTm" data-op=">=" data-options="showSeconds:false" class="easyui-datetimebox txtTm1">
    结束时间
    <input type="text" name="createTm" data-op="<" data-options="showSeconds:false" class="easyui-datetimebox txtTm2">
    快捷时间选择
    <select class="txtTmRange">
        <option value ="近8周">近8周</option>
        <option value ="近6月">近6月</option>
    </select>

    各种过滤条件:
    性别:
    <select name="sex">
        <option value ="">全部</option>
        <option value ="男">男</option>
        <option value ="女">女</option>
    </select>
    地域:
    <select name="region" class="my-combobox" data-options="valueField:'id',textField:'name',url:WUI.makeUrl(...)"></select>

    统计项
    <select id="cboRes">
        <option value ="COUNT(*) 总数">数量</option>
        <option value="SUM(t) sum">调用时间(毫秒)</option>
        <!-- 可以指定多个字段,逗号分隔,表示显示多个系列(这时下面的“分类汇总”是无效的),示例:
        <option value ="totalMh 理论工时,totalMh1 实际工时,totalMh2 出勤工时">工时</option>
        -->
    </select>

    汇总字段:
    <select id="g">
        <option value ="">无</option>
        <option value ="sex">性别</option>
        <option value="region">地域</option>

        <!-- 可以指定两个字段,逗号分隔,格式"分组字段,分组显示字段"
        <option value="userId,userName">用户</option>
        <option value="itemId,itemName">物料</option>
        -->
    </select>

    <input type="button" value="生成" class="btnStat"/>

    时间维度类型:
    <select id="tmUnit">
        <option value="y,w">周报表</option>
        <option value="y,m">月报表</option>
    </select>

    统计图:
    <div class="divChart" data-ac="User.query"></div>
</div>

遍历jpage中带name属性的各组件,生成统计请求的参数,调用接口获取统计数据并显示统计图表.

初始化示例:

var statItf_ = WUI.initPageStat(jpage, setStatOpt);

function setStatOpt(chartIdx, opt) 
{
    // this是当前chart的jquery对象,chartIdx为序号,依此可针对每个chart分别设置参数

    // 设置查询参数param.ac/param.res/param.cond等
    var param = opt.queryParam;
    param.cond += ...;
    param.res = jpage.find("#cboRes").val();

    // 设置时间维度,汇总字段
    opt.tmUnit = jpage.find("#tmUnit").val();
    opt.g = jpage.find("#g").val();

    // 设置echarts参数
    var chartOpt, seriesOpt;
    if (chartIdx == 0) {
        chartOpt = { ... };
        seriesOpt = { ... };
    }
    else if (chartIdx == 1) {
        chartOpt = { ... };
        seriesOpt = { ... };
    }

    $.extend(true, opt, {
        chartOpt: chartOpt,
        seriesOpt: seriesOpt,
    });
}

@param setStatOpt (chartIdx, opt) 回调设置每个chart. this为当前chart组件,chartIdx为当前chart的序号,从0开始。

@param opt= {tmUnit?, g?, queryParam, chartOpt, seriesOpt, onLoadData?, maxSeriesCnt?, formatter?, x?, useResOnly?}

统计模型

x表示横坐标字段,y表示纵坐标字段,g表示要转置到列上的字段(pivot字段)

参数运用:

@param opt.tmUnit Enum. 时间维度

如果非空,则按时间维度分析,即按指定时间类型组织横轴数据,会补全时间。参考[JdcloudStat.tmUnit]()
如果设置,它会自动设置 opt.queryParam 中的gres/orderby参数。

@param opt.g 分组字段名

会影响opt.queryParam中的gres选项。

有时分组字段使用xxxId字段,但希望显示时用xxxName字段,这时可在g中包含两个字段。

示例,按场景分组显示日报表:

opt.tmUnit = "y,m,d"; // 日报表
opt.g = "sceneId,sceneName";

这样生成的opt.queryParam中:

gres="y,m,d,sceneId";
orderby="y,m,d";
res="sceneName,COUNT(*) sum";

@param opt.queryParam 接口查询参数

可以设置ac, res, gres, cond, orderby, pagesz等筋斗云框架通用查询参数,或依照接口文档设置。
设置opt.tmUnit/opt.g会自动设置其中部分参数。

此外 ac, res参数也可通过在.divChart组件上设置data-ac, data-res属性,如

<div class="divChart" data-ac="Ordr.query" data-res="SUM(amount) sum"></div>

关于接口返回数据到图表数据转换,参考rs2Stat函数:

@see WUI.rs2Stat

@param opt.chartOpt, opt.seriesOpt

参考百度echarts全局参数以及series参数: http://echarts.baidu.com/echarts2/doc/doc.html

@param opt.onLoadData (statData, opt) 处理统计数据后、显示图表前回调。

this为当前图表组件(jchart对象)。常用于根据统计数据调整图表显示,修改opt中的chartOpt, seriesOpt选项即可。

@param opt.maxSeriesCnt ?=10

最多显示多少图表系列(其它系列自动归并到“其它”中)。参考rs2Stat函数同名选项。

@param opt.formatter

对汇总数据进行格式化显示。参考 rs2Stat函数同名选项。

@see WUI.rs2Stat

@return statItf= {refreshStat(), setTmRange(desc)} 统计页接口

refreshStat()用于显示或刷新统计图。当调用initPageStat(jpage)且jpage中有.btnStat组件时,会自动点击该按钮以显示统计图。
setTmRange(desc)用于设置jpage中的.txtTm1, .txtTm2两个文本框,作为起止时间。

@see WUI.getTmRange

@see WUI.initChart 显示图表

也可以调用WUI.initChart及WUI.rs2Stat自行设置报表,示例如下:

var jchart = jpage.find(".divChart");
var tmUnit = "y,m"; // 按时间维度分析,按“年月”展开数据
var cond = "tm>='2016-1-1' and tm<'2017-1-1'";
// WUI.useBatchCall(); // 如果有多个报表,可以批量调用后端接口
// 会话访问量
callSvr("ApiLog.query", function (data) {
    var opt = { tmUnit: tmUnit };
    var statData = WUI.rs2Stat(data, opt);
    var seriesOpt = {};
    var chartOpt = {
        title: {
            text: "访问量(会话数)"
        },
        legend: null
    };

    WUI.initChart(jchart[0], statData, seriesOpt, chartOpt);
}, {res: "COUNT(distinct ses) sum", gres: tmUnit, orderby: tmUnit, cond: cond });

此外,也支持直接显示无汇总的数据。

示例:有以下接口:

RecM.query() -> tbl(who, tm, cpu)

返回 服务器(who)在每一分钟(tm)的最低cpu使用率。数据示例:

{ h: ["who", "tm", "cpu"],
  d: [ 
    ["app", "2018-10-1 10:10:00", 89],
    ["app", "2018-10-1 10:11:00", 91],
    ["db", "2018-10-1 10:10:00", 68],
    ["db", "2018-10-1 10:11:00", 72]
  ]
}

由于tm已经汇总到分钟,现在希望直接显示tm对应的值,且按服务器不同("app"表示"应用服务器","db"表示"数据库服务器")分系列显示。查询:

RecM.query(res="tm,who,cpu", cond="...")

JS示例:

function initPageRecMStat()
{
    ...
    var statItf_ = WUI.initPageStat(jpage, setStatOpt);
    function setStatOpt(chartIdx, opt) 
    {
        var param = opt.queryParam;
        param.res = "cpu"; // y字段
        opt.x = "tm"; // x字段
        opt.g = "who", // g字段即图表系列(pivot字段)
        opt.useResOnly = true; // 直接显示原始数据,无须用gres分组,所以设置useResOnly.

        opt.formatter = function (value, arr, i) {
            var map = {
                "app": "应用服务器",
                "db": "数据库服务器"
            };
            return map[value] || value;
        };
    }
}

@module pageSimple

1 示例1:列表页上加查看报表按钮

2 示例2:先弹出查询条件对话框,设置后再显示报表

3 示例3:菜单上增加报表,且定制报表列实现交互

4 辅助列

5 显示统计图

6 冻结列/固定列

用于快捷展示报表。

1 示例1:列表页上加查看报表按钮

在订单列表上添加“月报表”按钮,点击显示订单月统计报表,并可以导出到Excel。

// function initPageCusOrder()
var btnStat1 = {text: "月报表", "wui-perm": "导出", iconCls:'icon-ok', handler: function () {
    var queryParams = jtbl.datagrid("options").queryParams;
    var url = WUI.makeUrl("Ordr.query", {
        gres:"y 年,m 月, userId",
        res:"userName 客户, COUNT(*) 订单数, SUM(amount) 总金额",
        hiddenFields: "userId",
        orderby: "总金额 DESC",
        pagesz: -1
    });
    WUI.showPage("pageSimple", "订单月报表!", [url, queryParams]);
}};
jtbl.datagrid({
    toolbar: WUI.dg_toolbar(jtbl, jdlg, ..., btnStat1),
    ...
});

注意:调用WUI.showPage时,标题以"!"结尾表示每次调用都刷新该页面。而默认行为是如果页面已打开,就直接显示而不会刷新。

注意:用pagesz:-1表示不分页,加载所有数据(实际上受限于后端,默认1000条)。

如果要支持分页,必须指定页码参数:page:1,如:

var url = WUI.makeUrl("Sn.query", {
    res: 'id 工件编号, code 序列号, code2 设备号, actualTm 工单开工, actualTm1 工单完工, itemName 产品名称, cateName 产品型号, ec 相关工程变更', 
    page: 1
});
WUI.showPage("pageSimple", "工件列表!", [url]);

url中接口调用返回的数据支持query接口常用的hd/list/array格式。

2 示例2:先弹出查询条件对话框,设置后再显示报表

常常与报表查询条件对话DlgReportCond一起使用, 先设置查询时间段,然后出报表,示例:

var btnStat1 = {text: "月统计", iconCls:'icon-ok', handler: function () {
    DlgReportCond.show(function (data) {
        var queryParams = WUI.getQueryParam({dt: [data.tm1, data.tm2]});
        var url = WUI.makeUrl("Capacity.query", { gres: 'y 年,m 月, name 员工', res: 'SUM(mh) 总工时, SUM(mhA) 总加班', pagesz: -1 });
        WUI.showPage("pageSimple", "出勤月统计!", [url, queryParams]);
    });
}};
jtbl.datagrid({
    toolbar: WUI.dg_toolbar(jtbl, jdlg, ..., btnStat1),
    ...
});

回调函数参数data是对话框中设置了name的输入字段,默认有tm1, tm2。

允许定制查询条件对话框,如添加查询字段,详见dlgReportCond对话框文档。

@see dlgReportCond

3 示例3:菜单上增加报表,且定制报表列实现交互

允许定制表格显示参数,如

WUI.showPage("pageSimple", "订单月报表!", [url, queryParams, onInitGrid]);

function onInitGrid(jpage, jtbl, dgOpt, columns, data)
{
    // dgOpt: datagrid的选项,如设置 dgOpt.onClickCell等属性
    // columns: 列数组,可设置列的formatter等属性
    // data: ajax得到的原始数据
}

注意:关于分页:如果url中有pagesz=-1参数,则不分页。也可直接设置dgOpt.pagination指定。

示例:菜单上增加“工单工时统计”报表,在“数量”列上可以点击,点击后在新页面中显示该工单下的所有工件明细。

菜单上增加一项:

            <a href="javascript:WUI.loadScript('page/mod_工单工时统计.js')">工单工时统计</a>

用WUI.loadScript可以动态加载JS文件,web和page目录下的JS文件默认都是禁止缓存的,因此修改文件后再点菜单可立即生效无须刷新。
在文件page/mod_工单工时统计.js中写报表逻辑,并对“数量”列进行定制:

function show工单工时统计()
{
    DlgReportCond.show(function (data) {
        var queryParams = WUI.getQueryParam({createTm: [data.tm1, data.tm2]});
        var url = WUI.makeUrl("Ordr.query", { res: 'id 工单号, createTm 生产日期, itemCode 产品编码, itemName 产品名称, cate2Name 产品系列, itemCate 产品型号, qty 数量, mh 理论工时, mh1 实际工时', pagesz: -1 });
        WUI.showPage("pageSimple", "工单工时统计!", [url, queryParams, onInitGrid]);
    });

    function onInitGrid(jpage, jtbl, dgOpt, columns, data)
    {
        // dgOpt: datagrid的选项,如设置 dgOpt.onClickCell等属性
        // columns: 列数组,可设置列的formatter等属性
        // data: ajax得到的原始数据
        $.each(columns, function (i, col) {
            if (col.field == "数量")
                col.formatter = formatter_数量;
        });
        // console.log(columns);

        // var btn = { text: "XX", iconCts: "icon-search", handler: ... };
        // 替换工具栏按钮
        // dgOpt.toolbar = WUI.dg_toolbar(jtbl, null, btn1);
        // 或是:追加工具栏按钮
        // dgOpt.toolbar.push.apply(dgOpt.toolbar, WUI.dg_toolbar(jtbl, null, btn1));
        // 不要工具栏
        // dgOpt.toolbar = null;
    }

    function formatter_数量(value, row) {
        if (!value)
            return;
        return WUI.makeLink(value, function () {
            var orderId = row.工单号;
            var pageFilter = {cond: {orderId: orderId }};
            WUI.showPage("pageSn", {title: "工件-工单" + orderId, pageFilter: pageFilter});
        });
    }
}

4 辅助列

以下划线结尾的列不显示,也不导出。

示例:显示工艺并可点击打开工艺对话框。

var url = WUI.makeUrl("Ordr.query", { res: 'id 工单号, flowId flowId_, flowName 工艺' });
WUI.showPage("pageSimple", "工单工时统计!", [url, null, onInitGrid]);

function onInitGrid(jpage, jtbl, dgOpt, columns, data)
{
    $.each(columns, function (i, col) {
        if (col.field == "工艺")
            col.formatter = Formatter.linkTo("flowId_", "#dlgFlow");
    });
}

上面flowId字段只用于链接,不显示,也不会导出。

5 显示统计图

在显示表格同时可以显示统计图。它与表格共用数据源。

// 也可以与pageSimple列表页结合,同时显示列表页和统计图:
var url = WUI.makeUrl("Ordr.query", {
    gres: "y 年,m 月",
    res: "count(*) 总数",
    orderby: "y,m"
});
var showChartParam = [ {tmUnit: "y,m"} ];
WUI.showPage("pageSimple", "订单统计!", [url, null, null, showChartParam]);

其中showChartParam必须指定为一个数组,即WUI.showDlgChart函数的后三个参数:[rs2StatOpt, seriesOpt, chartOpt]

@see showDlgChart 显示统计图

6 冻结列/固定列

如果列很多,会显示滚动条。
通过在queryParams中设置frozon,可指定前几列可以设置为冻结列,不跟随滚动条。

示例:

WUI.showPage("pageSimple", "订单月报表!", [WUI.makeUrl("Ordr.query"), {frozen: 1}]);

类似地,若要冻结前2列,则指定{frozen: 2}

@module dlgReportCond

DlgReportCond.show(onOk, meta?, inst?);

用于显示查询对话框,默认包含tm1, tm2字段对应用户输入的开始、结束时间。
可以用WUI.getQueryCond生成查询条件,它支持任意一个字段为空的情况下也能生成正确的条件表达式。

DlgReportCond.show(function (data) {
    var cond = WUI.getQueryCond({tm: [data.tm1, data.tm2]});
    console.log(data, cond); // 示例: data={tm1: "2021-1-1 8:00", tm2: ""}, cond="tm>='2021-1-1 8:00'"
})

也可以增加自定义字段:

var meta = [
    // title, dom, hint?
    {title: "状态", dom: '<select name="status" class="my-combobox" data-options="jdEnumMap:OrderStatusMap"></select>'},
    {title: "订单号", dom: "<textarea name='param' rows=5></textarea>", hint: '每行一个订单号'}
];
DlgReportCond.show(function (data) {
    console.log(data); // data中增加了status和param字段(如果没有填写则没有)
    var cond = WUI.getQueryCond({tm: [data.tm1, data.tm2], status: data.status, param: data.param});
    console.log(cond);
}, meta)

如果有多个不同的使用场景,对应的自定义字段不同,可以指定实例名来实现,如订单和工单的查询条件有些不同,可分开写:

DlgReportCond.show(function (data) {
    console.log(data);
}, meta1, "订单");

DlgReportCond.show(function (data) {
    console.log(data);
}, meta2, "工单");

如果完全自定义对话框(连默认的日期选项也不要),可直接使用showDlgByMeta

var meta = [
    {title: "序列号", dom: '<input name="code" class="easyui-validatebox" required>'}
];
WUI.showDlgByMeta(meta, {modal:false, reset:false, title: "设备生命周期", onOk: async function (data) {
    var pageFilter = {cond: data};
    WUI.showPage("pageSn", {title: "设备生命周期-工件!", pageFilter: pageFilter});
}});

示例2: 对话框上面有计划日期、硫化区域、尺寸三个定制字段,其中计划日期是日期字段,通过{data: {dt: xxx}}为它指定初始值;
“硫化区域”是个下拉框,使用my-combobox组件,通过jdEnumMap指定下拉列表项。在onOk回调中生成查询url并以pageSimple来显示。
在onInitGrid中定制了工具栏,dgOpt.toolbar置空表示不要工具栏了。

window.MachineMap = {
    "~B*": "B区",
    "~R*": "C区",
    "~D*": "D区"
};
var meta = [
    {title: "计划日期", dom: '<input name="dt" class="easyui-datebox" required>'},
    {title: "硫化区域", dom: '<input name="machine" class="my-combobox" data-options="jdEnumMap: MachineMap">'},
    {title: "尺寸", dom: '<input name="size">'}
];
var dt = new Date().addDay(-1); // 默认前1天
WUI.showDlgByMeta(meta, {modal:false, title: "生产计划查询", data: {dt: dt.format("yyyy-mm-dd")}, onOk: function (data) {
    var url = WUI.makeUrl("Plan.query", data);
    WUI.showPage("pageSimple", "生产计划!", [url, null, onInitGrid]);
}});

function onInitGrid(jpage, jtbl, dgOpt, columns, data) {
    // 列表页不显示工具栏
    dgOpt.toolbar = null;
}

@see showDlgByMeta

@module pageTab 一个多Tab页面

在一个页面中显示一个或多个Tabs组件(如果有多个Tabs,支持上下或左右排列),每个Tabs又可放置多个独立的页面,即:

<tabsA>
`- <tabA1-page1> <tabA2-page2>

<tabsB>
`- <tabB1-page3> <tabB2-page4>

用法示例:

// 显示1个tabs组件,id为"设备生命周期_1"(之后showPage要用); title加"!"后缀表示再次打开时刷新该页面
WUI.showPage("pageTab", "设备生命周期!"); 

// 两个tabs组件,上下各50%,每个tabs的id分别为"设备生命周期_1"和"设备生命周期_2"(之后showPage要用)
WUI.showPage("pageTab", {title: "设备生命周期!", tabs: "50%,50%"});

// 两个tabs,左右各50%
WUI.showPage("pageTab", {title: "设备生命周期!", tabs: "50%|50%"});

在指定的Tabs中显示页面可以用:

// 在第1个Tabs中显示页面, 用target参数指定tabs
WUI.showPage("pageSn", {target: "设备生命周期_1"} );

// 在第1个tabs中再显示1个页面
var url = WUI.makeUrl("Ordr.query", {
    gres:"y 年,m 月, userId",
    res:"userName 客户, COUNT(*) 订单数, SUM(amount) 总金额",
    hiddenFields: "userId",
    orderby: "总金额 DESC"
});
WUI.showPage("pageSimple", {target: "设备生命周期_1", title:"订单月报表!"}, [url]);

// 在第2个Tabs中显示页面
WUI.showPage("pageUi", {target: "设备生命周期_2", uimeta: "售后工单"}); 

注意:target以pageTab指定的title开头,再加上"_1"等后缀;
特别地,若title中含有空格、"-"等符号,则只取符号前的字符。
比如title="工单 - 1",则target可以用"工单_1"或"工单_2";而title="工单1"时,target可以用"工单1_1", "工单1_2"

Generated by jdcloud-gendoc