# portalTemplate - 门户模板扩展
# 简介
SuccBI中内置了几个常见的门户模板,但是无法满足个性化的门户需求,因此需要开发一些个性化的门户模板。门户模板扩展可以决定门户的整体结构和布局,并支持配置额外的属性。已安装的门户模板,可以在新建门户应用时选用,或者在门户设计器中即时切换。门户模板扩展属于前端扩展,支持的开发语言为typescript、javascript、css、less。
# 开发步骤
# 安装开发环境
扩展开发环境的安装可参考文档:扩展开发。
# 设计好门户的平面效果
开发门户模板前需要让设计师设计好门户的平面效果,确定门户的整体结构和布局。
# 选择合适的模板新建扩展
使用vscode新建扩展,在扩展的开发环境中执行命令succ create extension
,然后选择开发语言,选择扩展点(portalTemplate
),然后可以选择3种基本模板来新建扩展:
- 经典门户模板(classic):顶部模块选项卡,左侧资源树
- 磁贴门户模板(colorBlock):类似于windows开始菜单
- 空白门户模板(empty):门户的整体布局可以自由设计
根据设计好的门户效果,选择合适的模板来创建扩展,生成的扩展代码里会自动继承对应的类,减少开发工作量。更多扩展开发命令,可参考文档SuccIDE。
# 门户模板扩展的代码结构
创建好扩展后,会自动生成一个扩展的目录结构:
images
用于存放模板需要的图片资源main.ts
模板的入口脚本文件,如果改名需同步更改package.json
中的配置main.less
模板的样式文件,在main.ts
中引用,如果改名需同步更改main.ts
里的引用package.json
扩展的配置文件,包括扩展的信息和扩展点的信息,不同的扩展点的配置也不同。该文件的详细结构可参考api中的extension-types.d.ts
thumbnail.png
扩展的缩略图,在package.json
中引用
main.ts
中就是扩展的实现脚本,里面会自动继承类BaseTemplatePage
,并初始化了基本的代码结构。(如果创建扩展时选择的是经典门户,则会继承PortalTemplatePage
)
门户模板的实现类默认继承了Component
类,Component
类是系统内部所有UI控件的基类,有一些默认的基本规范需要注意:
- 应该在
_init_default
方法中初始化成员变量. - 应该在
_init
方法中构造dom结构,this.domBase
为控件最外层的dom,this.domParent
为控件的父dom - 需要实现
dispose
方法来销毁控件,dispose
方法中最后要调用父类的dispose
方法
生成的main.ts
中也会有较详细的注释。
# 开发门户模板的渲染逻辑
我们把通过js来构造、修改html/css等导致用户界面发生变化的过程称为渲染。门户模板的基本实现思路就是数据驱动渲染。
渲染时需要用到的所有数据在miniapp-types.d.ts
的TemplatePageInfo
接口中有详细定义。如果接口中定义的数据不能满足需求,不够个性化,可参考配置门户模板参数来配置额外的属性。
开发过程中可通过门户的数据对象TemplatePageDataBuilder
来获取元数据,如获取门户背景:
// this 为 BaseTemplatePage
this.builder.settings.background;
// 或
this.builder.getProperty('background');
门户模板需要实现两个接口,一个全量渲染doRefresh()
和一个增量渲染doRequestRender(undoItem: UndoItemInfo)
。
增量渲染用于设计器中,根据UndoItemInfo
中记录的变化的属性来选择性的渲染UI,如ColorBlock模板中doRequestRender
接口的实现:
public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
let comp = undoItem.c;
// 判断是否是资源发生变化
if (comp instanceof ResourceNodeBuilder) {
if (undoItem.op === UndoOperationType.Modify) {
// 修改了资源节点
} else {
// 增加或者删除了资源节点
}
} else {
let settings = this.builder.settings;
// undoItem.p中记录了变化的属性名,根据属性名决定ui如何更新
switch (undoItem.p) {
case 'logo':
let logo = settings.logo;
if (logo) {
this.setLogoImg(this.domLogo, logo.logoImage);
this.setLogoTitle(this.domTitle, logo.logoTitle);
}
break;
case 'allowGotoHome':
this.homeBtn.setVisible(settings.allowGotoHome);
break;
case 'allowLogout':
this.logoutBtn.setVisible(settings.allowLogout);
break;
case 'background':
this.setBackground(this.domBg, settings.background);
break;
}
return Promise.resolve();
}
}
如果需要获取资源节点的属性,则应该先获取对应资源节点的数据对象ResourceNodeBuilder
:
public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
let comp = undoItem.c;
if (comp instanceof ResourceNodeBuilder) {
// 获取节点用于渲染的数据
comp.toData().then(data=>{
});
}
}
如需获取所有的资源(用来渲染列表、树等),可直接使用数据对象的fetchData
接口。如ColorBlock模板中渲染磁贴的实现:
private refreshBlocks(): Promise<void> {
return this.builder.fetchData().then(datas => {
let blocks = this.blocks;
blocks.forEach(block => block.dispose());
blocks.clear();
datas.forEach(data => {
blocks.set(data.id, new ContentBlock({
domParent: this.domContent,
data: data
}));
});
});
}
在开发门户模板时,往往只有一个页面对象是不够用的,页面内的部分子模块也需要定义为类,页面在渲染时,需要调用子对象的渲染方法,所以推荐所有子对象都实现ITemplatePagePartRenderer
接口,该接口在templatepages.d.ts
中有定义,同样也是主要实现两个方法,一个全量渲染一个增量渲染,让页面对象来调用,如经典门户模板中增量渲染的实现:
public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
return waitAnimationFrame().then(() => {
/**
* 经典门户页面包括上方的标题栏headbar和下方模块页面modulePages,
* 所以增量更新时需要分别调用他们的updateRender方法,让子对象自己去处理自己的渲染。
*/
let proms: Array<Promise<void>> = [this.headbar.updateRender(undoItem), this.modulePages.updateRender(undoItem)];
return Promise.all(proms).then(() => {
let settings = this.builder.settings;
let p = undoItem.p;
switch (p) {
case 'background':
this.setBackground(this.domBg, settings.background);
break;
case 'template':
this.setTheme(settings.template.theme);
break;
}
if (undoItem.c instanceof ResourceNodeBuilder || undoItem.items) {
return this.navigate({ params: {} }, true).then(() => { });
}
});
});
}
获取到数据后就需要根据数据来渲染ui,基类上提供了三个常用的工具方法,包括设置背景色(setBackground
)、设置logo(setLogoImg
)、设置标题(setLogoTitle
)。
# 配置门户模板参数
扩展的信息和门户模板的扩展参数都需要配置在package.json
文件中,该文件的结构在extension-meta-types.d.ts
中有详细的定义(ExtensionInfo
)。
如果门户模板想要一些额外的属性,并且可在属性栏中进行设置,那么可以在扩展中配置extraProperties
属性,分为content
,style
,action
三类,分别对应属性栏中的三个面板(内容、样式、交互),其中propertyName
是直接记录在元数据中的属性名,propertyType
表示该属性的类型,不同的类型的属性在属性栏中对应的控件不同,返回的数据结构也不同。常见的类型有:
propertyType | 数据类型 | 说明 |
---|---|---|
checkbox | boolean | 勾选框 |
edit | string | 文本输入框 |
numberInput | number | 数值输入框 |
spinner | number | 数值微调框 |
combobox | string | 下拉框 |
fill | FillInfo | 背景填充控件,包括颜色填充、渐变填充和图片填充。FillInfo 需要使用基类上的setBackground 方法设置到dom上,如this.setBackground(this.domBg, data.background); |
icon | IconInfo | 图标编辑控件,包括大小和颜色。IconInfo 可使用基类上的setIcon 方法给dom设置图标,如this.setIcon(domIcon, data.icon) |
colorButton | string | 颜色按钮,点击后弹出颜色选择面。返回的字符串为主题色,需要使用基类上的getConvertedColor 方法转为css颜色,如dom.style.color = this.getConvertedColor(data.color) |
更多的类型可参考属性栏ppteditor.d.ts
defaultValue
表示该属性的默认值,值的类型需要和上面的类型对应。
配置的额外属性在属性栏中显示的标题,需要配置国际化,国际化key的格式为扩展名.propertyCaption.属性名
,如: succ-portalTemplate-colorBlock.propertyCaption.background
。关于如何配置国际化,可参考如何在代码中进行国际化。
# 发布测试
扩展开发过程中可能需要边测试边开发,实时观察模板的效果。这时就需要使用命令succ publish to server
将扩展发布到BI服务器上,发布后扩展就会立即生效,且会实时监听本地文件的变化,自动同步到服务器上。详情请参考扩展插件使用。发布后可打开小应用的设计器(新建或者编辑小应用)切换模板来查看效果。
# 示例
# 类windows磁贴模板的示例
创建门户模板扩展时如果选择磁贴门户模板,则生成的模板代码里会有一个较完整的门户模板实现:ColorBlockTemplate
,可参考该类的实现。
# 常见问题解答
# 如何识别资源结构变化和样式属性变化
门户在增量渲染时通常需要知道是资源变化还是页面样式变化,因为资源变化需要做的处理往往较多,那么应该如何区分呢?
门户资源有单独的数据对象ResourceNodeBuilder
,可以根据UndoItemInfo中记录的控件是否是该类的实例来判断:
public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
// 门户资源可批量操作,此时items为UndoItemInfo的数组
let items = undoItem.items;
if (items || undoItem.c instanceof ResourceNodeBuilder) {
} else {
}
}
# 如何快速跳转到定义该接口的地方
开发扩展时想看看接口的定义和注释来了解如何使用,如何快速跳转到定义该接口的地方?
在vscode的开发环境中,按住ctrl用鼠标点击变量、方法、类、接口等都可以直接跳转到定义的位置,也可使用ctrl + shift + O
在当前文件中搜索,或者ctrl + T
在工作区中搜索。
# 如何在门户中打开一个资源
首先获取需要打开的资源的数据,可通过数据对象ResourceNodeBuilder
或者fetchData
方法(参考开发门户模板的渲染逻辑),然后使用基类上的openFile
方法获取打开的资源对应的Component
对象(如Dashboard
),需要自行决定显示位置并管理显示隐藏等逻辑。同一个id多次调用该方法返回的是同一个对象。销毁一个不再使用的对象,需要调用deleteViewer
方法。
# 如何使用less
开发扩展时会自动将less编译成css放在同级目录下,ts中import编译后的css就行。
# 如何基于默认的模板进行定制开发
默认的模板是比较常见的经典布局,很多个性化的模板都可以直接基于默认的模板来修改。创建扩展时选择经典门户模板即可自动继承默认模板的实现
- 如果不需要修改dom结构和交互行为,可不用更改ts脚本,通过覆盖css来调整样式。
- 如果需要修改页面的某个部件,如左侧资源树,可重载对应的类(
ResourcesTree
),然后重写PortalTemplatePage
中对应的create方法(createResourcesTree
)。
# 如何配置模板默认的logo、标题、背景等
package.json中可配置模板初始化的默认数据(defaultPageInfo
),这些数据会初始化到属性栏中。如果不想显示在属性栏中,可以用样式定制,或者在渲染时做默认的处理。
# 如何实现多主题
一个模板支持多个主题,不同主题一般只有配色不同,实现多主题需要做:
- 在package.json中配置模板支持的主题信息。
- 实现
setTheme
方法。 - 用less或者css实现各主题的样式。
其中实现setTheme
方法需要注意:
- 需要调用父类的
setTheme
方法。如果有子控件,需要调用子控件的setTheme
方法,方便重载样式。 - 如果门户中当前有打开的资源(如dashboard),需要先设置该资源查看器的主题,由于设置查看器主题的方法
setPreferredThemes
是异步的,为了避免主题切换时不同步,需要先等待查看器切换主题完成再切换模板的主题。
如默认经典模板中的实现:
public setTheme(theme: string) {
if (this.theme === theme || theme !== 'dark' && theme !== 'light') {
return;
}
/**获取当前打开的资源查看器的方法 */
let activeViewer = this.getActiveViewer();
if (!activeViewer) {
super.setTheme(theme);
this.headbar.setTheme(theme);
this.modulePages.setTheme(theme);
this.storeTheme();
return;
}
/**获取主题配置信息,用于设置查看器的主题 */
this.builder.getTemplateThemeInfo(theme).then(info => {
let preferredThemes = info && info.preferredThemes || [theme, 'default'];
// 先等viewer切换theme完成,避免dashboard切换theme延迟的问题
activeViewer.setPreferredThemes(preferredThemes).then(() => {
super.setTheme(theme);
this.headbar.setTheme(theme);
this.modulePages.setTheme(theme);
this.storeTheme();
});
});
}