---
navTitle: portalTemplate
---
# portalTemplate - 门户模板扩展

\[\[toc]]

## 简介

SuccApp 内置了几个常见的门户模板。需要个性化门户结构和布局时，可以开发门户模板扩展，并为模板配置额外属性。已安装的门户模板，可以在新建门户应用时选用，或者在门户设计器中切换。门户模板扩展属于前端扩展，支持的开发语言为 TypeScript、JavaScript、CSS 和 Less。

## 开发步骤

### 安装开发环境

扩展开发环境的安装可参考文档：[扩展开发](../README.md)。

### 设计好门户的平面效果

开发门户模板前需要让设计师设计好门户的平面效果，确定门户的整体结构和布局。

### 选择合适的模板新建扩展

在 SuccApp for VS Code 命令面板中执行 **SuccApp: 从模板新建扩展**，选择扩展点 `portalTemplate`、模板和扩展名称；也可以使用 SuccApp CLI：

```bash
succapp workspace extension create portalTemplate <template> <name>
```

`portalTemplate` 当前可以选择 3 种基本模板：

1. 经典门户模板(classic)：顶部模块选项卡，左侧资源树
2. 磁贴门户模板(colorBlock)：类似于windows开始菜单
3. 空白门户模板(empty)：门户的整体布局可以自由设计

根据设计好的门户效果，选择合适的模板来创建扩展，生成的扩展代码里会自动继承对应的类，减少开发工作量。更多扩展开发命令，可参考[扩展平台能力](../../ai-building/extension/extension.md)。

### 门户模板扩展的代码结构

创建好扩展后，会自动生成一个扩展的目录结构：

- `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控件的基类，有一些默认的基本规范需要注意：

1. 应该在`_init_default`方法中初始化成员变量.
2. 应该在`_init`方法中构造dom结构，`this.domBase`为控件最外层的dom，`this.domParent`为控件的父dom
3. 需要实现`dispose`方法来销毁控件，`dispose`方法中最后要调用父类的`dispose`方法

生成的`main.ts`中也会有较详细的注释。

### 开发门户模板的渲染逻辑

我们把通过js来构造、修改html/css等导致用户界面发生变化的过程称为渲染。门户模板的基本实现思路就是数据驱动渲染。

渲染时需要用到的所有数据在 `miniapp-types.d.ts` 的 `TemplatePageInfo` 接口中有详细定义。如果接口中定义的数据不能满足需求，不够个性化，可参考[配置门户模板参数](#配置门户模板参数)来配置额外的属性。

开发过程中可通过门户的数据对象`TemplatePageDataBuilder`来获取元数据，如获取门户背景：

```typescript
// this 为 BaseTemplatePage
this.builder.settings.background;
// 或
this.builder.getProperty('background');
```

门户模板需要实现两个接口，一个全量渲染`doRefresh()`和一个增量渲染`doRequestRender(undoItem: UndoItemInfo)`。

增量渲染用于设计器中，根据`UndoItemInfo`中记录的变化的属性来选择性的渲染UI，如ColorBlock模板中`doRequestRender`接口的实现：

```typescript
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`：

```typescript
public doRequestRender(undoItem: UndoItemInfo): Promise<void> {
	let comp = undoItem.c;
	if (comp instanceof ResourceNodeBuilder) {
		// 获取节点用于渲染的数据
		comp.toData().then(data=>{

		});
	}
}
```

如需获取所有的资源（用来渲染列表、树等），可直接使用数据对象的`fetchData`接口。如ColorBlock模板中渲染磁贴的实现：

```typescript
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`中有定义，同样也是主要实现两个方法，一个全量渲染一个增量渲染，让页面对象来调用，如经典门户模板中增量渲染的实现：

```typescript
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`。关于如何配置国际化，可参考[如何在代码中进行国际化](../README.md)。

### 发布测试

扩展开发过程中通常先推送到测试服务器，再打开门户设计器观察模板效果。可以在 SuccApp for VS Code 中执行 **SuccApp: 推送扩展**，也可以使用 CLI：

```bash
succapp workspace extension push <extension-path>
```

推送成功后，打开门户设计器（新建或编辑门户应用）切换模板来查看效果。更多同步和验证流程见[AI 低代码开发](../../ai-building/README.md)。

## 示例

### 类windows磁贴模板的示例

创建门户模板扩展时如果选择磁贴门户模板，则生成的模板代码里会有一个较完整的门户模板实现：`ColorBlockTemplate`，可参考该类的实现。

## 常见问题解答

### 如何识别资源结构变化和样式属性变化

门户在增量渲染时通常需要知道是资源变化还是页面样式变化，因为资源变化需要做的处理往往较多，那么应该如何区分呢？

门户资源有单独的数据对象`ResourceNodeBuilder`，可以根据UndoItemInfo中记录的控件是否是该类的实例来判断：

```typescript
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`方法需要注意：

1. 需要调用父类的`setTheme`方法。如果有子控件,需要调用子控件的`setTheme`方法，方便重载样式。
2. 如果门户中当前有打开的资源（如dashboard），需要先设置该资源查看器的主题，由于设置查看器主题的方法`setPreferredThemes`是异步的，为了避免主题切换时不同步，需要先等待查看器切换主题完成再切换模板的主题。

如默认经典模板中的实现：

```typescript
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();
		});
	});
}
```
