定项目名为demo2,使用jdcloud_ganlan作为项目模板,用git_clone.sh工具(在jdcloud-ganlan的tool目录下有)创建新项目。
git_clone.shjdcloud-ganlandemo2注意:不要直接用gitclone命令,在git_clone.sh工具中调用了gitclone并做了专门设置。
cdtoolmakemeta(将会修改server/tool/upgrade/META文件,应将它提交到git库)注意:windows版本make工具从这里下载\server-pc-mingw.zip,解压到可执行目录(如d:,将它加到系统环境变量PATH中),确保可以执行命令make。
在线创建数据模型:
在开发过程中,常常要修改表或字段,也可以用以下命令来升级数据库:
cdtoolshupgrade.shinitdb但最终上线前应通过makemeta来更新meta文件,以便于新环境下在线部署。在线创建或更新数据库也可以直接访问:
关于虚拟字段vcol,一般是即可用于结果展示(res)又可用于查询条件(cond)。对虚拟字段的复杂介绍,也可像表字段一样列在下面。比如设计cusName字段:
@CusOrder:id,...vcol:cusName(Customer.name)比如要查询张姓客户的所有订单及姓名:
classAC2_CusOrderextendsAccessControl{...protected$vcolDefs=[["res"=>["cus.namecusName"],"join"=>"LEFTJOINCustomercusONcus.id=t0.cusId","default"=>true]]}特别地,还有一些虚拟字段,只用于查询条件或添加,习惯上分别写为vcolforcond和vcolforadd。
vcolforcond常用于一对多关联中,用多方字段来查一方的表。比如查询某个订单的客户,或某时段内下单的所有客户,即用CusOrder的tm字段来过滤Customer表,可在Customer上设置orderId、orderTm虚拟字段:
callSvr("Customer.query",{cond:{orderId:1001}});callSvr("Customer.query",{cond:{orderTm:">=2020-1-1AND<2021-1-1"},distinct:1});//加distinct参数可避免重复实现示例:
classAC2_CustomerextendsAccessControl{...protected$vcolDefs=[...["res"=>["o.idorderId"],"join"=>"JOINCusOrderoONt0.id=o.cusId",]]}vcolforadd常用于在添加时用于检查和添加关联对象,例如导入订单时(batchAdd/add接口),不传cusId字段,代之以cusName字段。要通过cusName找到Customer的id即cusId,甚至找不到时自动创建Customer。这时可以将cusName设计为vcolforadd:
vcolforadd:cusName(Customer.name)实现示例:
点击“生成”将自动生成页面文件(pageXXX.html/js,dlgXXX.html/js)并加入git库。
定义用到的固定下拉列表变量(enum对应的XXList/XXMap)或动态下拉列表函数(如ListOptions.Employee),在web/store.js中定义:
classAC2_CustomerextendsAccessControl{protected$vcolDefs=[["res"=>["emp.nameempName"],"join"=>"LEFTJOINEmployeeempONemp.id=t0.empId","default"=>true]];}在页面web/store.html中调整菜单位置:
注意检查自动生成的图片和附件组件的选项:
先在DESIGN.md文档中描述接口,像get/query/set/add/del等标准接口,可以忽略标准接口参数,只写些特别的逻辑:
后端代码api_objects.php(称为AutoComplete/自动补全逻辑)
//initDlgCustomerWUI.setDlgLogic(jdlg,"disableFlag",{valueForAdd:0//添加时自动设置为0});4.2为选项设置颜色当disableFlag为1时(客户被禁用),单元格显示为灰色:
在客户页面中,只需要为toolbar指定一个导入按钮:pageCustomer.html
functioninitPageCustomer(){...jtbl.datagrid({...toolbar:WUI.dg_toolbar(jtbl,jdlg,"import","export"),//添加"import"即可})}4.4添加模糊查询在客户页面中,为toolbar指定一个模糊查询按钮,称为qsearch,有两种方法。
方法一:前后端协同。后端指定模糊查询的字段。
functioninitPageCustomer(){...jtbl.datagrid({...toolbar:WUI.dg_toolbar(jtbl,jdlg,"import","export","qsearch"),//添加"qsearch"即可})}为了支持对客户的模糊查询,应在后端Customer对象中支持qsearch(自动在指定的若干字段中模糊匹配):api_objects.php
//classAC2_Customer中protectedfunctiononQuery(){//添加qsearch支持$this->qsearch(["id","name","code"],param("q"));}注意:在下拉选择列表中,过滤功能也会用到qsearch,这时只能写在后端。
方法二:直接由前端指定模糊查询的字段(v6支持)pageCustomer.js
functioninitPageCustomer(){...jtbl.datagrid({...toolbar:WUI.dg_toolbar(jtbl,jdlg,"import","export",["qsearch","id,name,code"]),//添加"qsearch"按钮})}5物流订单基本步骤与前面类似。
在列表页pageCusOrder.html中配置颜色,状态列(status):
在dlgXXX.html/js中设置添加时的只读字段,与客户管理中做的类似。
在DESIGN.md中定义接口逻辑:
classAC2_CusOrderextendsAccessControl{protected$vcolDefs=[["res"=>["cus.namecusName","cus.empIdcusEmpId"],"join"=>"LEFTJOINCustomercusONcus.id=t0.cusId","default"=>true],["res"=>["emp.nameempName"],"join"=>"LEFTJOINEmployeeempONemp.id=t0.empId","default"=>true],["res"=>["emp1.namecusEmpName"],"join"=>"LEFTJOINEmployeeemp1ONemp1.id=cus.empId","require"=>"cusEmpId","default"=>true]];protectedfunctiononValidate(){if($this->ac=="add"){$_POST["tm"]=date(FMT_DT);$_POST["empId"]=$_SESSION["empId"];$_POST["status"]="CR";}}}自动生成的AC1_CusOrder类用不到,将其删除掉即可。
在store.html中修改菜单项,把“物流订单管理”改成“物流订单”(习惯上不超过4个字):
指定添加时必传字段,以及不可修改字段:在api_objects.php中以Customer为例:
classAC2_CustomerextendsAccessControl{protected$requiredFields=["cusId","type"];protected$readonlyFields=["tm","empId"];...}在dlgCustomer.html中设置validatebox:
可在创建文件时直接用required选项指定:
先刷新数据模型(meta):
cd/var/www/srcgit_init.shdemo2注意:git_init.sh是jdcloud项目tool目录下自带的工具。在本地git中将myserver:src/demo2设置为库地址,将源码push上去。打上链接以便web可访问:
cd/var/www/htmlln-sf/var/www/src/demo2/server./demo2初始化配置和数据库:
典型的1对多关系展示:
在pageCustomer.js中添加“查看订单”按钮:
先为Customer对象设计虚拟字段:orderCnt,orderSum在后端实现它:
classAC2_CustomerextendsAccessControl{protected$vcolDefs=[...["res"=>["(SELECTCOUNT(*)FROMCusOrderWHEREcusId=t0.id)orderCnt","(SELECTSUM(amount)FROMCusOrderWHEREcusId=t0.id)orderSum"],"default"=>true]];}在管理端客户列表页显示这两个字段:pageCustomer.html
varCustomerUi={orderCnt:function(value,row){if(!value)returnvalue;returnWUI.makeLink(value,function(){varpageFilter={cond:{cusId:row.id}};WUI.showPage("pageCusOrder",{title:"物流订单-客户"+row.id,pageFilter:pageFilter});});}}WUI.makeLink生成一个链接,点击时执行回调函数,用showPage显示订单页面,并通过pageFilter参数传递了过滤条件。
为了支持按客户信息过滤,需要增强一下订单页,在上一节中已做过处理。此处打开过滤后的订单页,使用的是完全相同的接口。
在客户对话框中以子表方式显示该客户的所有订单,在这里也可以直接添加订单。
客户对话框中添加子表:pageCustomer.html
使用wui-subobj组件,obj选项指定子对象为“CusOrder”,relatedKey选项指定关联字段为“cusId”。valueField的值对应后端AC类中子表名,如果不加这个属性,则添加主表数据时子表是不可用的;dlg选项指定订单详情对话框,也可以不指定,这样子表的增加、修改等操作就不可用了。
要支持CusOrder的各项操作,后端应有AC2_CusOrder类。
valueField选项指定可调用Customer.add()(...,orders)接口,在添加主表记录的同时,添加子表。后端须实现orders子表:
classAC2_CustomerextendsAccessControl{protected$subobj=[..."orders"=>["obj"=>"CusOrder","cond"=>"cusId={id}"],]}这样子表就做好了。
在客户列表中显示该客户处于各状态的订单数,如“已签收:2,已结算:20”。这是对上面订单数的增强。在Customer对象上设计虚拟字段orderCntStat。
实现思路是先定义子表字段:
classAC2_CustomerextendsAccessControl{protected$subobj=["orderCntStat"=>["obj"=>"CusOrder","cond"=>"cusId={id}","gres"=>"status","res"=>"COUNT(*)cnt","default"=>true]];}测试:callSvr("Customer.query",{res:"id,orderCntStat"})这样查询出的orderCntStat字段格式为:[{status:"RE",cnt:2},{status:"CL",cnt:20}]
为了便于前端显示,需要将子表数组转成一个字符串。这可以在前端来做(通过datagrid的formatter定制),也可以在后端来做(通过enumFields机制,称为枚举字段,或是计算字段)。
在后端做的好处是支持导出数据(比如导出Excel等),如果在前端用formatter来渲染数据,导出时是不执行的formatter的。
示例:在后端做这个格式转换定制:
在列表页中添加这列:pageCustomer.html
上面是用子表加上自定义处理实现,还有一种方案,是直接用SQL定义虚拟字段,这需要对SQL子查询非常熟悉,参考实现如下:
如果不想手写那么长CASEWHEN,也可以调用sqlCaseWhen函数把map转成caseWhen语句,由于类成员赋值时不能直接调用函数,可以放在onInit里面做:
classAC2_CustomerextendsAccessControl{...protected$vcolDefs=[...["res"=>["u_orderCntStat(t0.id)orderCntStat"],"default"=>true],];其中u_orderCntStat就是一个自定义SQL函数,按惯例加u_表示自定义函数;按惯例应将它的定义写在data/init.sql文件中,以便部署时使用。
实现导出月报表,即每月的订单数和总金额。其本质是一个统计查询接口,即带gres参数的query接口。
classAC2_CusOrderextendsAccessControl{protectedfunctiononInit(){$this->vcolDefs[]=["res"=>tmCols("t0.tm")];}}现在就可以直接测试一下月统计接口了:
callSvr("CusOrder.query",{gres:"y,m",res:"COUNT(*)cnt,SUM(amount)totalAmount"})若还要增加按客户分组,可以再分组参数中再加上cusId,并将字段结果换成中文,便于前端展现:
callSvr("CusOrder.query",{gres:"y年,m月,cusId",res:"cusName客户,COUNT(*)订单数,SUM(amount)总金额",orderby:"总金额DESC",hiddenFields:"cusId"})分组字段使用cusId(编号)比用cusName(名字)效率高,为了让cusId不出现在最终结果中,使用了参数hiddenFields来指定。
要导出Excel格式,只需在查询参数中再加上fmt:'excel'即可。
已封装好的的逻辑页pageSimple,以及查询对话框DlgReportCond,可以方便的用于展示报表:
现在点击“导出月报表”按钮,就可以下载Excel文件了。
检查角色,可以用g_data.hasRole,这是比较简单的控制,是直接在代码中写死的;后面章节还要讲到权限,是可以通过对角色进行配置而无须编码的,也可以控制到每个操作按钮;或者也可以通过WUI.canDo(null,"报表分析")进行编码控制。
系统默认已有表头左上角右键菜单中隐藏地集成了这一功能,要让它显示在列表工具栏,只要简单地加一个名为“report”的按钮:pageCusOrder.js
functioninitPageCusOrder{...vardgOpt={url:WUI.makeUrl("Ordr.query"),//toolbar:WUI.dg_toolbar(jtbl,jdlg,btn1,"export","report","qsearch"),//加一个"report"即可toolbar:WUI.dg_toolbar(jtbl,jdlg,btn1,"export",g_data.hasRole("mgr")&&"report","qsearch"),//为"report"加上角色控制:mgr-最高管理员...};jtbl.datagrid(dgOpt);}点开统计分析,实现以下分析:
勾上“复制代码”,点确定;然后创建新文件server/web/page/mod_订单分析.js,按Ctrl-V粘贴得到类似下面的代码:
打开store.html,在菜单中添加一项:
使用WUI.loadScript的好处是,修改js源文件后,无须刷新,直接点菜单就可以查看更新后的效果。
(已过时,仅供参考)
可以参考示例server/web/adm/pageApiLogStat.html(.js),这类页面的命名规范是page{obj}Stat,所以这是对ApiLog对象的分析。可以将逻辑页复制过来,将对象名字改成CusOrder,得到pageCusOrderStat.html(.js)。对其中内容添加或修改必要的分组字段即可。
在store.html中加个菜单:
先来实现:管理员只能查看和操作自己添加的客户及其订单。
要限制用户能操作的内容,一般通过后端在onQuery里加查询限制条件实现,这里即是对客户的empId字段进行限制:
classAC2_CustomerextendsAccessControl{protectedfunctiononQuery(){if(!hasPerm(PERM_MGR))$this->addCond("empId=".$_SESSION["empId"]);}}对订单(CusOrder)的查询限制,须先关联到客户上,再根据客户的empId字段过滤,这就要用到虚拟字段cusEmpId:
classAC2_CusOrderextendsAccessControl{protected$vcolDefs=[[//下面将使用虚拟字段cusEmpId进行过滤"res"=>["cus.namecusName","cus.empIdcusEmpId"],"join"=>"LEFTJOINCustomercusONcus.id=t0.cusId","default"=>true],];...protectedfunctiononQuery(){if(!hasPerm(PERM_MGR))$this->addCond("cusEmpId=".$_SESSION["empId"]);}}这样,前端不用修改,即可实现不同角色看到的内容不同。
要实现“订单分析”页只能最高管理员看,只需在菜单上配置一下,添加类“perm-mgr”,表示“mgr”(即最高管理员)可见:
在jdcloud-ganlan中已默认添加了角色插件。可以忽略本节。
先支持角色定义,通过插件role来实现,可参考其配置文档:server/plugin/role/DESIGN.md开发步骤章节。摘录如下:
在主设计文档中包含插件:
@includeserver\plugin\role\DESIGN.md用于创建表和字段。
后端应在plugin/index.php中设置:
$GLOBALS["P_initClient"]["enableRole"]=true;前端可使用g_data.initClient.enableRole检查是否开启本特性。
在管理端主菜单“系统设置”中添加“角色管理”:
在客户列表页pageCustomer.js中,定制字段显示:
//在initPageCustomer函数中,当初始化datagrid后,定制字段显示jtbl.datagrid(...)//用g_data.hasRole来判断角色。这里isAdm就表示有mgr或emp权限之一,即管理员以上权限varisAdm=g_data.hasRole("mgr,emp");WUI.toggleFields(jtbl,{picId:isAdm,atts:isAdm});在客户对话框dlgCustomer.js中定制对话框上的字段显示,设置各字段的disabled属性:
functioninitDlgCustomer(){//varisMgr=g_data.hasRole("mgr");varisAdm=g_data.hasRole("mgr,emp");vardisabled=!isAdm;WUI.setDlgLogic(jdlg,"code",{disabledForSet:disabled});WUI.setDlgLogic(jdlg,"name",{disabledForSet:disabled});WUI.setDlgLogic(jdlg,"disableFlag",{disabledForSet:disabled,valueForAdd:0});WUI.setDlgLogic(jdlg,"结算周期",{disabledForSet:disabled});//设置子表只读而不是`disabled`,否则就看不了子表了。注意子表名`orders`是通过在subobj组件中定义CSS类`wui-subobj-orders`来关联的。WUI.setDlgLogic(jdlg,"orders",{readonly:disabled});//图片和合同字段不可见WUI.setDlgLogic(jdlg,"picId",{show:isAdm});WUI.setDlgLogic(jdlg,"atts",{show:isAdm});}为了按名字设置子表orders,dlgCustomer.html中为子表加上wui-subobj-orders类标识它的名字是orders(按惯例,名字须与valueField选项一致):
functioninitDlgCustomer(){jdlg.on("beforeshow",onBeforeShow);...functiononBeforeShow(ev,formMode,opt){varobjParam=opt.objParam;varforAdd=formMode==FormMode.forAdd;varforSet=formMode==FormMode.forSet;setTimeout(onShow);varisAdm=g_data.hasRole("mgr,emp");WUI.toggleFields(jfrm,{picId:isAdm,atts:isAdm});//注意:这里限制只在forSet(设置)模式且是非管理员用户时,才做禁止编辑操作。避免forAdd或forFind(查询模式)也受影响。varval=forSet&&!isAdm;$(frm.结算周期).prop("disabled",val);$(frm.code).prop("disabled",val);$(frm.name).prop("disabled",val);$(frm.disableFlag).prop("disabled",val);//让子表也只读。这是设置WUI组件选项的方法:WUI.getOptions(jdlg.find(".wui-subobj-orders")).readonly=val;...}}设置字段值一般在onShow中写逻辑(包括初始化显示datagrid表格)。若要隐藏字段,或设置字段是否可编辑,一般在onBeforeShow中写逻辑。也可以都到onShow中设置。
页面逻辑建议通过WUI.setDlgLogic来定义,更为清晰。请参考手册了解详细。仅当用setDlgLogic解决不了,可以考虑使用以上传统方式。
将物流订单按类别不同细分为包车发运、散货发运和其它订单三个子菜单,放在“订单管理”菜单下。点击菜单打开相应类型的订单页,注意列表页面和详情对话框的标题与类型显示一致。
类似前面讲过的关联页面,可以通过pageFilter机制来打开继承自pageCusOrder页面的“包车发运”页:
WUI.showPage("pageCusOrder",{title:"包车发运",pageFilter:{cond:{type:"包车发运"}}});这样菜单可以定义为: