@windwolf
2017-04-19T05:06:27.000000Z
字数 7477
阅读 424
Sailing
Sailing的UI结构需要足够灵活,能搭建ERP场景下的各种UI结构;需要足够的可重用性,可以层层复用各种业务组件;需要足够易用,可以让实施顾问在尽量少的技术支持下自行完成UI的调整。
因此,我们需要一个UI模型,这个模型可以由基本构件自由组成而成;而构件也可以根据业务需要扩展,只需要遵循UI模型的基本规范;这个UI模型还有一套易用的表示方法。我们可以用LEGO积木来做类比。LEGO积木块本身十分简单,但确可以以此搭建出无穷无尽的各种形象。
LEGO积木块有很多种,但他们共同遵循一套基本规范,就是用圆型凸起来连接,圆形突起有相同的尺寸和间隔要求。UI构件就是LEGO积木块,UI构件有很多种,但遵循共同的容器布局和接口要求。
大多数的LEGO积木块还同时有公母接口,这样就可以用小小的积木搭出复杂的大型玩具。UI模型就一样,可以用层次结构搭建出各种复杂的UI页面。
LEGO还很容易搭建,他的结构设计使得小朋友的力量就可以任意拆分组合;而一个大型的模型可以由多人分块负责,最后总成在一起。UI模型的表示也一样。
先从UI模型的基本构件讲起。总起来说,一共有三类UI构件:Control、Component和Layout。
ControlValueAccessor
接口的Angular组件,可以在Form体系中绑定到某个对象字段上。Control构件大量用于表单中,用于呈现字段内容并负责用户交互。使用以上三类UI构件层层组合,可以用一棵UI构件树来表示大多数ERP中的UI。
以Demo中的商品详情页为例。商品详情页可以分解为以下UI构件树:
以下为用元数据表示的构件树
{
"index" : NumberInt(0),
"size" : {
"column" : NumberInt(3)
},
"type" : "group",
"items" : [
{
"type" : "control",
"index" : NumberInt(0),
"path" : "id",
"label" : "id",
"uiType" : "sys#id",
"editable" : false
},
{
"type" : "control",
"index" : NumberInt(2),
"hidden" : false,
"path" : "商品编码",
"label" : "商品编码",
"uiType" : "sys#text",
"editable" : true
},
{
"type" : "tab",
"size" : {
"column" : NumberInt(0)
},
"index" : NumberInt(3),
"panels" : [
{
"label" : "基本信息",
"root" : {
"index" : NumberInt(0),
"size" : {
"column" : NumberInt(3)
},
"type" : "group",
"items" : [
{
"type" : "control",
"index" : NumberInt(3),
"hidden" : false,
"path" : "品名",
"label" : "品名",
"uiType" : "sys#text",
"editable" : true
},
{
"type" : "control",
"index" : NumberInt(4),
"hidden" : false,
"path" : "型号",
"label" : "型号",
"uiType" : "sys#text",
"editable" : true
},
{
"type" : "control",
"index" : NumberInt(7),
"hidden" : false,
"path" : "销售计量单位",
"label" : "销售单位",
"uiType" : "sys#unit",
"editable" : true
}
]
}
},
{
"label" : "供应商信息",
"root" : {
"type" : "control",
"size" : {
"column" : NumberInt(0),
"row" : NumberInt(5)
},
"index" : NumberInt(8),
"hidden" : false,
"path" : "商品供应商",
"uiType" : "sys#grid",
"editable" : true,
"inputs" : [
{
"target" : "page",
"value" : NumberInt(0)
},
{
"target" : "columns",
"value" : [
{
"path" : "id",
"label" : "id",
"uiType" : "sys#id",
"hidden" : true,
"width" : NumberInt(1),
"index" : NumberInt(0),
"linkable" : false,
"disable" : false,
"editable" : false
},
{
"path" : "工厂货号",
"label" : "工厂货号",
"uiType" : "sys#text",
"hidden" : true,
"width" : NumberInt(1),
"index" : NumberInt(2),
"linkable" : false,
"disable" : false,
"editable" : false
},
{
"path" : "生产厂家",
"label" : "生产厂家",
"uiType" : "sys#text",
"hidden" : false,
"width" : NumberInt(100),
"index" : NumberInt(3),
"linkable" : false,
"disable" : false,
"editable" : false
}
]
}
]
}
},
{
"label" : "统计信息",
"root" : {
"index" : NumberInt(0),
"size" : {
"column" : NumberInt(3)
},
"type" : "group",
"items" : [
{
"type" : "control",
"index" : NumberInt(1),
"hidden" : false,
"path" : "统计信息.总销量",
"label" : "总销量",
"uiType" : "sys#amount",
"editable" : true
},
{
"type" : "control",
"size" : {
"column" : NumberInt(1),
"row" : NumberInt(1)
},
"index" : NumberInt(9),
"hidden" : false,
"path" : "统计信息.最近采购数量",
"label" : "最近采购量",
"uiType" : "sys#amount",
"editable" : true
}
]
}
}
]
}
]
}
层次上的一个Json对象,记录了UI构件树中的一个UI构件。整个文档就表示出了整个UI构件树。文档中除了表达了UI构件的层次结构外,还有些额外的属性。下面来一一解释。
所有的UI构件都有四个基本属性:
- id(可选):赋予某个UI构件一个id,便于在应用中查找。
- size(可选):UI构件容器的大小,以行列为单位。不同的UI构件有不同的默认值规则,在【布局】章节详述。
- index:在父构件中相对兄弟构件所处的位置。
- hidden(可选):默认false。是否不显示。
凡是需要数据绑定的场合,UI的最终目的无非两个:一是用于呈现绑定的数据;一是通过与用户的交互获取数据。一般来说,业务含义相似的字段,呈现和交互方式也是相似的。因此,有需要定义一些有业务类型针对性的构件,以便统一用户体验,增加复用性。
Control以及Cotrol构件就是为此而生。
一个Control由两部分组成:
ControlValueAccessor
接口的Angular组件,它可以用于Form数据绑定。一个Control就是一个标准的支持Form的Angular组件,它和UI模型没有任何耦合关系。Control可以根据需要绑定到任意类型,包括基本类型(string、number、boolean)、对象和数组。Control可以包含其他Control,来达到复用的目的。
作为约定,Control的select用q-
开头,以-control
结尾。
ControlContainer的目的十分单一,因此都具有类似如下结构:
@Component({
selector: 'q-xxx-control-container',
template: `
<div [formGroup]="status?.form.parent" class="ui-g-12 form-group">
<q-xxx-control [formControlName]="status?.objectName"></q-xxx-control>
</div>
`,
})
export class XxxControlContainer implements ControlContainer {
public status: ControlUiItemStatus;
}
作为约定,ControlContainer的select为对应Control的select加-container
后缀。
- type(control):用于标示Control构件类型。
- inputs(可选):为Control构件绑定初始Input属性值。
- outputs(可选):为Control构件绑定Output属性处理器。
- path:绑定字段的业务模型名称。
- objectPath:绑定字段的对象属性名称。
- label(可选):名称标题。
- uiType:构件的uiType类型。它确定了具体使用哪个Control。具体在【UiType】章节展开。
- required:是否必填。
- editable:是否可编辑。
- calculators(可选):字段计算规则。有关字段计算规则的介绍,请看【表单】专题说明。
有些情况下,会需要用到一些UI组件,但这些组件无需绑定业务数据。这类UI构件就是Component。
一个Component就是一个标准Angular组件。
为了能在UI模型中以配置的方式使用,需要在使用前在响应模块的providers中使用provideComponents
注册一个ComponentMetadata
,任何Angular组件都可以注册为Component。
ComponentMetadata的属性如下:
- id: 在UI模型中使用这个这个id的模块限定名来引用Component。
- type: Component构件的类。必须是一个Angular组件类。
- size: 默认大小,如果Component构件中没有指定size,则使用这个size。如果这个size也没有,这默认为(column:1,size:1)。
- type(component):用于标示Component构件类型。
- inputs(可选):为Control构件绑定初始Input属性值。
- outputs(可选):为Control构件绑定Output属性处理器。
- component:构件的Component类型。可以是任何已经注册的Component。
以行列为单位按行流式排列其中的构件。
- type(grid):用于标示Grid构件类型。
- items:UI构件数组。
- type(tab):用于标示Tab构件类型。
- panels:
TabPanel
数组。
- label:tab页的标签。
- root:放置在tab页内的UI构件。
未实现
没有专属属性
某种业务含义类型的字段在不同的场景中需要有不同的呈现方式。例如计量单位类型在表单中表现为一个AutoComplate,在列表字段中保险为一个名称加类型的缩写。为了尽量使实施顾问能从较高层次配置系统,因此有必要抽象出一个UiType的概念,一个UiType包含了某种业务字段类型在表单、表格、搜索框等各种UI场景中的相关配置。以下就是一个UiType的例子:
{
id: "date",
form: {control: "sys#datepicker"},
simpleSearch: {control: "sys#datespan", queryGenerator: QUERY_TEXT_LIKE}
}
上例定义了一个适用于日期类型的UiType,这个UiType的名称为date,表单情况下使用名为sys#datapicker的Control,这个字段在搜索框中则采用sys#dataspan组件。
- form(可选):用于表单环境的一组配置
- control:用于表单环境的控件
- size(可选):大小.
- inputs(可选):控件的输入绑定
- outputs(可选):控件的输出绑定
- simpleSearch(可选):用于搜索框环境。
- control:。用于搜索框环境的控件
- queryGenerator:搜索表达式产生器。
- inputs(可选):控件的输入绑定
- outputs(可选):控件的输出绑定
- table(可选):用于表格环境
- formatter(可选):单元格文字格式化器。
- width(可选):列宽。
- control(可选):行内编辑模式的控件。
- inputs(可选):控件的输入绑定
- outputs(可选):控件的输出绑定
一般来说,ERP中的一个页面常常需要呈现很多的数据,对单位大小信息密度的要求很高。因此ERP中的UI布局要求紧凑、简洁、整齐、高效,而对特型布局的要求。
在这个前提下,UI模型需要将UI开发人员从像素级布局中解放出来,因此引入了基于行列的布局方式。UI开发者首选指定页面的整体列数,例如采用3栏式还是4栏式等,确定了整体列数之后,每个UI构件仅需确定自身所占行数列数以及顺序即可,具体定位由布局算法自动完成。
每种UI构件的自身大小确定方式如下:
Empty:
NOTE:如果column指定为0,则表示占用容器中的整行,而不管容器实际列数。根容器不允许将column设为0。