1 引言
前面由浅入深介绍了表单控件的基本开发方法、开发了两个最简单控件:“简单控件”和“进阶控件”,在这个过程中,涉及到利用DWF提供的自动识别配置选项,添加事件和触发事件调用操作。
本章将结合一个多对象列表展示控件:“简单列表—simpleCellGroup”,通过开发这个控件,介绍如何在控件,了解如何增加自定义配置选项,数据绑定的实现方法。
当对象拖入表单以后,用户可以将标题属性和内容属性分别对接到类的属性上,用户可以设置单击事件嵌入自身的逻辑,如下图所示:
这个控件利用iView中的单元列表控件—Cell开发,单元列表控件的效果可以参考:https://www.iviewui.com/components/cell,此外,作为对上一章内容的巩固,我们还会为此表单控件增加一个单击事件,允许用户为事件编写脚本。
在详细介绍控件开发方法之前,先了解一下DWF中的,控件是怎么在表单定制工具里访问数据的。
2 模型端数据访问
关于数据访问的问题,我们在第五章 操作插件开发-进阶中介绍了this.dwf_ctx的概念,它代表了一切函数的总入口并且和上一部分的脚本开发里提供几乎同样的函数.。
同样的,在建模工具中也包含了这样的入口,通过this.dwf_ctx可以访问。在 前端脚本和插件开发调用的对应关系中详细介绍了这些在建模工具中可以使用的函数和对应的例程。
下面就this.dwf_ctx包含的几个关键的函数进行介绍:
2.1 获取模型数据
首先,是this.dwf_ctx中关于模型数据获取的函数:
- getAttribute(AttributeName):获取属性对应的JSON对象
- getAttributesByClass(className):获取类对应的属性集合
- getEntityClass(targetClass):获取当前DWF内的所有实体类
- getRelationClass(targetClass):获取当前DWF内的所有关联类
- isEntitySysAttribute(attribute):判断attributes是否为实体类系统属性, 返回值为-1时为非系统属性,其余为系统属性
- isRelationSysAttribute(attribute):判断attribute是否为实体类系统属性, 返回值为-1时为非系统属性,其余为系统属性
例如:判断attributes是否为实体类系统属性, 可以这样调用:
let assetAttrs = this.dwf_ctx.getAttributesByClass("Asset");
assetAttrs.forEach(a => {
if (this.dwf_ctx.isEntitySysAttribute(a)){
console.log(a);
}
});
2.2 获取对象数据
在建模工具一侧,如果需要是访问可以通过下面的函数
- handleQueryData(queryData):通过${queryData}获取数据,通过回调将结果异步返回。
其中,queryData默认等待传入的属性包括:
- targetClass: 指定目标类
- query: 表示目标对象查询条件的参数设定,具体包括如下的属性:
- query: 表示目标查询条件,例如:and obj.id = "xxx",关于查询语句的语法详见: DWF内部查询条件语法
- startIndex: 翻页有关,从第几条开始
- pageSize: 翻页有关,每页大小
- fresh: 是否从前端缓存加载,默认不从后端加载,当为false的时候从后端重新刷新加载
例如:访问一个设备类(Asset)对象的写法可以是:
let targetClass = "Asset";
let queryParams = {
query: "and obj.name = 'xxx'",
pageSize: 4,
startIndex: 0,
};
this.handleQueryData({
targetClass: targetClass,
query: queryParams,
fresh: true,
}).then((res) => {
...
});
如果希望同步获得结果,可以通过在外部函数设定asnyc标记,然后在内部加上await实现,具体写法是:
async getData(){
let targetClass = "Asset";
let queryParams = {
query: "and obj.name = 'xxx'",
pageSize: 4,
startIndex: 0,
};
let objs = await this.handleQueryData({
targetClass: targetClass,
query: queryParams,
fresh: true,
});
}
接下来,开始进入控件的开发。
3 准备基础代码框架
参照 第六章 表单控件开发-入门和 第七章 表单控件开发-进阶,两章介绍,在part-web/modeler/forms以及part-web/app/forms文件夹,分别新建一个名为simpleCellGroup.vue文件。
在part-web/modeler/forms/里的实现未来准备被表单建模工具调用
- 控件区、画布去预览和选项设置的界面,利用EditBox引用,实现dwf内置属性的自动识别。
- 与表单建模工具的数据交换函数,包括:props引用,getArgs/setArgs,setDisplayType,getEditBox,getFormName等。
- 最后为了方便在无法查询到数据的时候进行显示,在vue文件里scope部分添加了一些样式。
然后,分别在part-web/modeler以及part-web/app文件夹对应的assemble-to.yaml文件里增加对应的装配选项,将简单列表放入多对象控件分组“form/multi"中:
4 增加控件配置选项和事件
在了解了modeler端数据访问的方法之后,可以回头看看列表控件应该提供的配置选项了。我们希望这个控件在绑定数据的时候可配置的选项如下:
4.1 控件args选项设计
在 第七章 表单控件开发-进阶,已经介绍DWF的表单引擎和表单建模工具之间配置选项是通过名为args对象实现模型数据交换的,对于简单列表这个控件来说,args属性可以复用dwf内置选项,还需要新增一些个性化选项:
复用内置选项
对于即将开发的列表控件而言,有些通用选项是可以直接利用dwf的保留选项实现的,可以参考 DWF可自动识别的args参数 了解可以直接使用的保留选项
- 名称(title):返回控件的名字,用于显示。
- 目标类(bindTargetClass):用于设置需要显示的类,这个属性的设置我们可以直接利用dwf提供的保留字实现,因此无需额外考虑编码。
- 整体标题(label):用于设置列表项外框的整体标题,这个属性页可以直接利用dwf提供的保留字实现,无需额外考虑外观的编码。
- 高度和类型(height,heightType):设置控件的整体高度和样式单位。
- 宽度和类型(width,widthType):设置控件的整体宽度和宽度单位。
- 背景色(back_color):控件的背景色。
- 文字色(txt_fontcolor):控件中文字的颜色。
增加交互事件
为了支持用户可以设置事件和操作的绑定关系,需要增加两个属性
- 事件清单(eventRange):表示预置的事件类型,是一个数组。
- 事件列表(events):用于存储用户设置的事件和操作的绑定。
增加个性选项
除了上述的选项之外,结合Cell控件自身的特点设置选项,在本章的例子中,设计两个属性
- 项目标题(attributeForTitle):用于设置列表项的标题,设置后每一个对象的指定属性会对接到列表项标题上,在dwf提供的保留字里没有此保留字,因此,需要新增一个选项。
- 项目内容(attributeForLabel):用于设置列表项的内容,设置后每一个对象的指定属性会对接到列表项内容上,同样,在dwf提供的保留字里没有可以复用的保留此,因此,需要新增一个选项。
- 项目图标(attributeForIcon):用于设置列表项的图表,设置后每个对象前面可以出现一个图标。
基于上述设计简单列表的控件在建模工具modeler侧和app侧的vue文件对应的配置选项的代码如下:
args: {
// 在选项区显示的名称
title: "简单列表",
// 直接复用DWF默认的配置项,从而无需自行开发配置界面
bindTargetClass: "",
// 标签作为列表标题
label: "",
// 建模设置的过滤条件
filterQuery: "",
// 高度取值和单位
height: "",
heightType: "px",
// 宽度取值和单位
width: "",
widthType: "px",
// 背景色设置
back_color: "",
// 文字颜色设置
txt_fontColor: "",
/* ---------------------
* 下面是新增的自定义属性
* -------------------- */
// 单元格显示标题
attributeForTitle: "",
// 单元格显示内容
attributeForLabel: "",
// 显示图标对应属性
attributeForIcon: "",
// 在配置区支持的事件列表,元素为事件中文名
eventRange: ["单击"],
// 已被用户设置的事件列表,元素格式为 { opr_type: '', opr_path: '', name: '事件中文名' }
events: [],
},
上述args对应的配置选项和事件,会在在控件被表单建模工具或者表单引擎内部实例化的时候,对应的setArgs()传递到控件内部。
4.2 加载对象数据
除了配置选项数据之外,简单列表控件还需要存储对象数据,以便将内容正确显示给用户,因此,在控件的vue文件中的data部分,增加一个cellData数组存储查询得到的对象,实现如下:
// Vue数据绑定的时候要求返回的结果
data() {
return {
// 插件的名字
name: "SimpleCellGroup",
// 表示是否已经进入画布区
t_preview: true,
// 是否显示控件的属性编辑区
t_edit: false,
// 属性配置项,按需设置
addIcon: true,
actEdit: true,
actIndex: -1,
setModal: false,
// 控件支持设置类型
dataTypes: ["String"],
// 属性配置项,按需设置
args: {
// 控件自带的选项等,这里忽略
...
},
cellData: {},
};
},
其中cellData是未来需要和前端标签部分绑定的数据,其格式如下:
// 需要显示数据的集合,包括标题,图标,单元标题,单元内容,约定格式如下
{
title: "雪橇三傻",
icon: "ios-bookmarks-outline", // see https://www.iviewui.com/components/icon
list: [
{
title: "一傻",
label: "哈士奇",
},
{
title: "二傻",
label: "萨摩耶",
},
{
title: "三傻",
label: "阿拉斯加",
}
],
}
这些对象数据应该在vue控件的生命周期函数mounted()中实现,在dwf中习惯通过freshData()作为控件刷新默认实现,因此需要在created()中提取表单的store属性,并且在mounted()函数中调用这个freshData()即可:
import { mapGetters, mapActions } from "vuex";
export default {
props: [
"addin",
"basicArgs",
"argsProps",
"activeUUID",
"store",
"itemValue",
"attributes",
"relation",
"editExtendInfo",
"widgetAnnotation",
"checkResult",
"query_oprs",
"route",
"router",
"root",
"Message",
"echarts",
],
... //省略部分代码
mounted() {
// 生命周期函数,实现数据加载
this.freshData();
},
created() {
// 将表单引擎传递下来的store赋值给当前控件的store,以便handleQueryData可以用到这个全局属性
this.$store = this.store;
},
computed: {
//...mapGetters("DWF_form", ["Entities", "Relations"]),
cardStyle() {
// 外层卡片的样式
const result = {};
if (this.args.height) {
result["height"] =
String(this.args.height) + (this.args.heightType || "px");
}
if (this.args.width) {
result["width"] =
String(this.args.width) + (this.args.widthType || "px");
}
if (this.args.back_color) {
result["backgroundColor"] = this.args.back_color;
}
if (this.args.txt_fontColor) {
result["color"] = this.args.txt_fontColor;
}
return result;
},
// 需要用到实体类属性列表时使用
filter_attributes() {
return [];
},
},
methods: {
// use `handleQueryData` to fetch data.
...mapActions("DWF_form", ["handleQueryData"]),
// 默认不用变化,返回编辑框供建模工具绘制,当控件拖入画布区以后被点击的时候触发
getEditBox() {
this.t_edit = true;
return this.$refs.edit;
},
// 当插件无法直接通过style设置高度时,使用setHeight方法设置高度
setHeight() {
if (!this.$refs.main) return;
},
// 现有表单加载在画布区加载控件的时候,会将之前的配置传入
setArgs(args) {
for (var i in args) {
this.args[i] = args[i];
}
return this;
},
// 表单保存的时候将控件的设置合并到表单自身的JSON中
getArgs() {
return this.args;
},
// 返回控件绑定的目标属性,如果没有绑定返回undefined或者null
getFormName() {
return this.args.name;
},
freshData() {
const bindTargetClass = this.args.bindTargetClass;
if (!bindTargetClass) {
console.error("no bindTargetClass");
return;
}
const regexForTargetClass = /^([0-9a-zA-Z_]+)&([er])$/;
const result = regexForTargetClass.exec(bindTargetClass);
if (result === null) {
console.error(
`handleRefresh: invalid targetClass value -> ${bindTargetClass}`
);
return;
}
let targetClass = result[1];
let query = {
query: this.args.filterQuery,
pageSize: 4,
startIndex: 0,
};
let objs = this.handleQueryData({
targetClass: targetClass,
query: query,
fresh: true,
}).then((res) => {
let objs = res;
this.cellData = {
title: this.args.label,
icon: "ios-bookmarks-outline",
list: res,
};
});
},
},
methods: {
// 使用 `handleQueryData` 获取数据
...mapActions("DWF_form", ["handleQueryData"]),
// 此处省略若干函数的实现直接接入正题...
freshData() {
// 获取已经绑定的实体类、关联类类名,此类名会有一个后缀以便表明其类型,例如实体类:Asset&e,关联类:PartToPart&r
const bindTargetClass = this.args.bindTargetClass;
if (!bindTargetClass) {
console.error("no bindTargetClass");
return;
}
// 在使用的时候,需要将后缀抹去,利用正则表达式实现如下
const regexForTargetClass = /^([0-9a-zA-Z_]+)&([er])$/;
const glob = regexForTargetClass.exec(bindTargetClass);
if (glob === null) {
console.error(
`handleRefresh: invalid targetClass value -> ${bindTargetClass}`
);
return;
}
// 经过正则过滤后,得到去除后缀的实体类/关联类的类名
let targetClass = glob[1];
let query = {
query: this.args.filterQuery,
pageSize: 4, //设计的时候可以根据需要设置翻页和大小
startIndex: 0,
};
this.handleQueryData({
targetClass: targetClass,
query: query,
fresh: true,
}).then((res) => {
const attributeForTitle = this.args.attributeForTitle;
const attributeForLabel = this.args.attributeForLabel;
let objs = res;
this.cellData = {
title: this.args.label,
icon: "ios-bookmarks-outline",
list: res,
};
});
},
},
};
4.3 设置动态样式
在前面章节里提到了前景色和背景色的设置,在即将开发的dwf控件里,样式的设置可以实现为一个计算属性,并将其绑定在卡片自身:style属性上,如下所示:
<template>
<!--
建模时的预览前端,即插件的实际显示样式
:addinName="name"和ref="main"一般情况不可去除
-->
<section v-if="t_preview" :addinName="name" ref="main">
<!-- 在画布区的显示内容 -->
<span>
<!-- 关于Cell控件的使用,详见:https://www.iviewui.com/components/cell#JCYF -->
<Card :style="cardStyle" class="cell-group-wrapper">
<!-- 省略代码.... -->
</Card>
</span>
</section>
<!-- 省略代码.... -->
</template>
<script>
//...
computed: {
cardStyle() {
// 外层卡片的样式
const result = {};
if (this.args.height) {
result["height"] =
String(this.args.height) + (this.args.heightType || "px");
}
if (this.args.width) {
result["width"] =
String(this.args.width) + (this.args.widthType || "px");
}
if (this.args.back_color) {
result["backgroundColor"] = this.args.back_color;
}
if (this.args.txt_fontColor) {
result["color"] = this.args.txt_fontColor;
}
return result;
},
},
methods: {
},
</script>
<style scoped>
/* 没有合适的数据的时候使用的样式 */
.no-result-prompt {
display: flex;
align-items: center;
justify-content: center;
background-color: #eeeeee;
color: #00000022;
font-weight: bold;
font-size: 2em;
}
/* hack to respond to height change.
see https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors
*/
.cell-group-wrapper >>> .ivu-card-body {
position: relative;
height: calc(100% - 60px);
}
.cell-group-wrapper >>> .ivu-cell-group {
height: 100%;
overflow-y: auto;
}
</style>
4.4 数据与标签的绑定
在表单定制工具中的标签
在表单建模工具中,args对应的配置界面可以直接利用dwf提供的EditBox组件自动实现,而个性化属性attrbuteForTile和attributeForLabel需要在EditBox标签内增加slot实现,具体的代码如下:
<template>
<!--
建模时的预览前端,即插件的实际显示样式
:addinName="name"和ref="main"一般情况不可去除
-->
<section v-if="t_preview" :addinName="name" ref="main">
<!-- 在画布区的显示内容 -->
<span>
<!-- see https://www.iviewui.com/components/cell#JCYF -->
<Card :style="cardStyle" class="cell-group-wrapper">
<p slot="title">
<!-- change to slot syntax to avoid a bug, where :title may not respond to change as a prop. -->
<Icon :type="cellData.icon"></Icon>
{{ cellData.title }}
</p>
<CellGroup v-if="cellData.list && cellData.list.length">
<Cell
v-for="obj in cellData.list"
:key="obj.oid"
:title="String(obj[args.attributeForTitle])"
:label="String(obj[args.attributeForLabel])"
>
<Avatar :src="String(obj[args.attributeForIcon])" slot="icon" />
</Cell>
</CellGroup>
<div v-else class="no-result-prompt">无数据展示...</div>
</Card>
</span>
<span v-show="t_edit" ref="edit">
<!-- v-bind. see https://vuejs.org/v2/api/#v-bind -->
<EditBox
v-model="args"
:router="router"
:route="route"
:root="root"
:itemValue="itemValue"
:query_oprs="query_oprs"
:targetclass="itemValue.data.targetClass"
>
<div slot="attribute">
<!-- 从外部引用的辅助标签,用于显示属性 -->
<p>标题属性(Title)</p>
<AttributeSelector
:target-class="args.bindTargetClass"
v-model="args.attributeForTitle"
></AttributeSelector>
<p>详情属性(Label)</p>
<AttributeSelector
:target-class="args.bindTargetClass"
v-model="args.attributeForLabel"
></AttributeSelector>
<p>图标属性(Icon)</p>
<AttributeSelector
:target-class="args.bindTargetClass"
v-model="args.attributeForIcon"
></AttributeSelector>
<Button type="primary" long @click="freshData">刷新数据</Button>
</div>
</EditBox>
</span>
</section>
<section v-else :addinName="name">
<span style="text-align: center">
<div class="form-addin-icon">
<!-- 控件拖放区图标的样式
see: http://ise.thss.tsinghua.edu.cn/font_857540_a2fo0rqai0f/demo_index.html-->
<i class="iconfont"></i>
</div>
<!-- 在控件拖放区图标的名字 -->
<div class="form-addin-name">简单列表</div>
</span>
</section>
</template>
在上述代码里:
- 预览区中会根据控件的配置数据从cellData里将内容显示出来。
- 选项设置区中利用EditBox将args的数据显示出来,其中使用了一个组件AttributeSelector用于帮助用户设置属性,详细内容 DWF提供的内置选项设置组件中间描述。
- 最后在图表区里,显示了控件的图标
在表单引擎中的标签
在app端的表单引擎里由于没有设置选项的界面,因此,标签可以大幅度简化,如下所示:
<template>
<!--
建模时的预览前端,即插件的实际显示样式
:addinName="name"和ref="main"一般情况不可去除
-->
<section :style="{
width: args.width + args.widthType,
}" :addinName="name" ref="main">
<!-- 在画布区的显示内容 -->
<span
:style="{
height: args.height+ args.heightType,
width: args.width + args.widthType,
}"
style="
font-size: 50px;
display: flex;
justify-content: center;
align-items: center;"
@click="onClick"
@dblclick="onDoubleClick"
>{{args.label}}</span>
</section>
</template>
表单引擎中的事件响应
在这里的<CellGroup>标签里,增加了@on-click的实现,对应控件onClick()函数,需要激活操作,具体的调用和 第七章 表单控件开发-进阶 提到的方法类似,在methods部分增加函数调用操作即可:
// 单击事件
onClick() {
this.triggerEvent("单击");
},
// 根据名字触发表单事件上配置的操作
triggerEvent(eventName) {
let eventConfig = null;
// 提取事件对应的操作参数
if (this.args.events && this.args.events.length > 0) {
eventConfig = this.args.events.find((val) => {
return val.name === eventName;
});
}
// 触发实际操作
if (eventConfig) {
this.invokeOperation(
eventConfig.opr_type,
eventConfig.opr_path,
this.itemValue,
this.store
);
}
},
5 增加供DWF脚本使用的方法
为了给脚本开发者提供支持,控件一般会提供一些函数供脚本开发者调用,例如:freshData(),getAll(),getSelected()等,在本章开发的简单列表控件中,只需要在app端methods部分提供对应的方法即可实现对脚本的支持,实现如下:
// Vue数据绑定的时候要求返回的结果
data() {
return {
// 插件的名字
name: "SimpleCellGroup",
// ...
// 需要显示数据的集合,包括标题,图标,单元标题,单元内容,约定格式如下
cellData: {},
// 记录被点击选择的对象
cellSelected: {},
};
},
methods: {
...
getSelected() {
return this.cellSelected;
},
getAll() {
return this.cellData.list;
},
// 单击事件,会传入被点击对象的索引,详见:https://www.iviewui.com/components/cell
onClick(idx) {
this.cellSelected = this.cellData.list[idx];
this.triggerEvent("单击");
},
}
6 结果展示
上述代码汇总在一起之后,通过装配,可以在建模工具内看到新增的多对象控件“简单列表”,选择类、标题属性、内容属性,然后点击刷新数据,即可看到数据显示的结果。
图-简单列表多对象控件
在控件中可以添加单击事件,例如:绑定一个简单的消息弹框的事件之后,显示的内容如下:
7 最终代码
7.1 建模工具
建模工具的代码如下
对应的配置文件如下:
config:
ignore:
info:
part-web:
name: modeler
cname: 建模端
forms:
SimpleCellGroup.vue:
icon: "md-apps"
cname: 高级控件
type: form/multi
operations:
mobileForms:
mobileOperations:
dependencies:{}
7.2 应用前端
对应的装配文件格式如下:
config:
ignore:
info:
part-web:
name: app
cname: 应用端
forms:
SimpleCellGroup.vue: form/multi
operations:
...
dependencies: {}
8 小结
本章主要结合vue列表控件介绍了如何在dwf中实现一个多对象控件。在这个过程中重点涉及了:
- 如何通过mapActions和mapGetters,在控件的表单模型定制工具中获取模型数据和对象数据。
- 如何为控件添加自定义选项,例如:标题对应属性、内容对应的属性,并且通过EditBox标签的slot标签绑定这些自定义选项。
- 如何按照选项设定的取值,将数据显示到控件之上。
- 最后,解释了如何在控件上添加脚本引擎中可以使用的函数。
在本章的实现中,还复习了一下如何为控件增加事件,并调用DWF的操作。

