视图插件开发文档
本文档是供开发者阅读的「视图」插件开发文档,需要开发者具备前端开发基础,掌握Javascript、CSS和HTML等相关知识。
如果开发者熟悉 React JS,也可以通过查看HAP前端开源项目 https://github.com/mingdaocom/pd-openweb ,参考HAP系统视图代码进行插件的开发。
关于视图插件
什么是视图插件?
视图插件又叫「自定义视图」,当HAP的表格、看板、层级、日历、画廊、详情、甘特图等系统视图不能满足用户视图展示需求的时候,开发者可以通过自己编写代码实现一个完全自定义的视图页面,用于展示工作表的记录数据。自定义视图支持搜索、筛选、统计、快速筛选和筛选列表等操作,还可以通过HAP公共Javascript接口实现调用系统组件,比如展示记录详情弹窗、调用新建记录窗口等等。
视图插件和系统视图有什么区别?
从使用者的角度看,视图插件和普通视图是没有任何区别的。当组织管理员通过发布开发者插件、安装插件或者导入插件后,所有已启用的插件即对组织下的所有用户生效,用户可以像使用表格、看板、日历等系统视图一样使用这些视图,也可以正常的为视图分配权限和进行视图分享等操作。
开发步骤
准备工作
- 安装 Node.js(>=16.20) 和 npm
- 准备集成开发环境(IDE),推荐 VS Code
- 如果是团队开发,请准备好代码版本管理工具,推荐 Git
创建视图插件
要创建一个视图插件,有两种方式。
1. 创建自定义视图
通过在新建视图时,创建一个「自定义视图」,此时系统会自动创建一个视图插件,并以当前工作表为该视图的开发调试环境。
2. 在插件中心制作插件
在系统首页进入「插件中心」
在插件「我开发的」页面中点击「制作插件」
通过此方式创建插件时,仍然需要选择一张工作表作为开发调试环境,选择后会自动在该工作表下创建一个新的视图用于开发调试视图插件。
创建好插件后,进入到工作表下新创建的这个自定义视图,可以进行下一步开发。
3. 插件需求分析
在制作视图插件之前,一定要对要开发的视图进行需求分析,明确视图的适用范围,并通过设计合理的设置项来提高视图插件的通用性。
比如,有两张工作表:订单和订单明细。
订单表 | 订单明细表 |
---|---|
现在开发者想自己开发一个视图,将「订单明细」的数据显示到主「订单」的表格中,主订单的数据将以合并单元格的方式同时展示两张表的数据,大概类似这样:
首先,在功能实现上,我们可以采取先加载主表格,再通过异步的方式获取子表数据进行加载。
其次,经过分析,这个视图插件如果要做到有一定的通用性,给任意一个工作表都能使用,那就需要增加一些使用者可以自由配置的内容:
- 这个视图表格的显示字段和顺序是允许使用者自行调整配置的;
- 一个工作表可能存在多张子表,那么就需要使用者配置要把哪张子表展示到主表格中;
通过对视图需求的整理,可以让用户更加明确开发的目标和实现的边界,也更容易将插件做到适应更多的通用场景,降低开发的成本。
4. 插件基础设置
i. 图标和名称
插件的名称建议能准确表达视图的作用,且不用带“视图”二字,比如:「地图」、「思维导图」、「树型表格」等等。
图标可以使用自定义图标,这个相当于插件的logo。
ii. 功能启用
视图插件允许用户自由选择是否启用「快速筛选」和「筛选列表」。当启用后,视图使用者在操作了快速筛选项和筛选列表后,系统将向插件发送事件触发消息,开发者需要在插件中添加事件处理句柄并通过传入的筛选条件处理数据筛选逻辑。
可以参考本文档附录中的 mdye 消息系统 示例代码。
iii. 定义视图设置项参数
主表显示字段 | 明细表字段 | 参数映射配置 |
在以上这个示例中,我们定义了两个视图设置项:「显示字段(showFields
)」和「子表明细字段(sub
)」,分别对应两个配置的需求。在「参数映射」中,开发者可以将实际的视图配置映射到字段里,并在代码中通过 env
变量获取到它的值。参考代码:
import { env } from "mdye";
const { showFields, sub } = env;
// showFields, sub 即为使用者配置的值,变量名称和配置中的变量ID一一对应
设置项参数有如下几种类型:
设置项类型 | 子类型 | 值类型 | 备注 |
---|---|---|---|
字段选择器 | 字段单选 字段多选 | array[string] | 字段多选时,可以限制选择字段的数量 |
字符串 | string | ||
数值 | double | ||
枚举值 | 单选框 下拉菜单 | array[string] | 选项格式为 key=value ,其中 vlaue 为呈现给使用者的文字,key 为代码中获取到的值; 样式为单选框时,可以选择横向或竖向排列; |
布尔值 | 开关 勾选框 | boolean | |
分组标题 | null |
5. 创建本地项目
接下来,切换到「开发调试」面板,我们将根据向导创建一个本地项目,并将本地项目运行在调试工作表中。
i. 选择脚手架模板
开始本地开发前,需要先选择一个内置的脚手架模板,在本地执行初始化命令时会创建对应的模板文件。目前系统提供了以下模板供选择:
- React 基础示例模板
- JavaScript 基础示例模板
- React + Tailwind CSS 模板
- Vue 3 模板
- Vue 2 模板
ii. 安装 mdye cli 命令行工具
本地项目的初始化创建是通过HAP的命令行工具 mdye
来实现的,所以需要事先全局安装这个工具。mdye
是 MingDaoYun Extensions 的首字母缩写。
请在计算机终端命令行用以下命令安装:
$ npm install -g mdye-cli
如果报没有权限的错误,请用 sudo
来安装:
$ sudo npm install -g mdye-cli
安装完成后,可以用下面的命令来验证是否安装成功:
$ mdye --version
beta-0.0.34
如果能正常输出版本号,则表示安装成功。这个工具的安装通常来说是一次性的,即后续开发新插件时无需再次安装该工具。如果该工具将来有新版本,则可以重新安装该工具进行升级。
mdye
完整的命令如下:
Usage: mdye [options] [command]
Options:
-v, --version 查看 mdye 版本
-h, --help 帮助
Commands:
auth [options] mdye auth 明道云授权登录
init [options] mdye init view --id <id> --template <template-name> 初始化项目,请从web端复制命令
start [options] mdye start 开始开发
build [options] mdye build
push [options] mdye push -m <message> 提交插件
whoami [options] mdye whoami 我是谁
logout [options] mdye logout 注销当前环境账户
sync-params [options] mdye sync-params -f <file-path> 同步插件参数配置,-f 非必填 默认文件路径为 ./.config/params-config.json
help [command] 子命令帮助
iii. 初始化本地项目
在「建立本地项目」步骤中复制创建插件本项目的命令,在本地终端中执行。
你可以自定义本地项目文件夹名称,直接回车则使用系统给定的文件夹名称。
接下来需要启动本地项目,我们先进入插件本地项目文件夹,然后打开 VS Code,接下来的所有插件开发操作都在VS Code中完成:
$ cd mdye_view_6541abe07a43f661079c234f #进入项目文件夹
$ code . #在 VS code 中打开项目
在VS Code中打开项目后,从菜单「终端>新建终端」新建一个「终端」窗口,依次输入以下命令:
$ npm i #安装项目依赖
$ mdye start #启动本地项目调试
执行之后,可以看到如下画面:
iv. 调试本地插件
项目运行成功后,会生成一个本地服务器js文件,把这个地址填入插件的「调试」页面,并点击「加载」:
此时,视图页面会动态渲染该视图插件,我们在初始化脚本里写一了些简单的和HAP工作表交互的方法:
你可以尝试修改 src/App.js
文件并保存,由于采用了热更新技术,所以代码修改保存后会在视图上实时生效。
v. 配置代码级环境参数
环境参数对于插件的作用,主要是存放一些在代码级别会用到的开发配置,用于将来插件上架后被安装时或导出到别的环境中时可以更换配置值。例如开发者在开发视图插件时使用了一个付费的第三方组件(前端),在开发时开发者填入的是自己的 license Key。开发者希望用户安装或导入插件后可以使用自己的 Key不要共享开发者的付费授权。此时就可以使用环境参数配置来处理。
但是这个配置不是视图的使用者要去关心的,也不必在每次使用插件视图时都配置这个 key。所以它是一个管理员级别的只需要配置一次的值。
这个参数是JSON格式,它会直接被注入在 mdye.env
的参数中,开发者在编写环境参数时,注意不要和配置项参数的ID重复。
6. 编写插件代码
i. 文件结构
视图插件是通过嵌入的 iframe 加载用户本地提交的脚本渲染页面来实现。
📢 需要特别注意的是,移动端的适配需要开发者自行实现。如果移动端有特殊的配置,也可以增加一些设置项参数来处理。开发者可以通过 UA 来判断是否处于移动端设备中。
ii. 代码调试
视图插件代码的开发和调试,和普通前端项目并无不同,开发者可以在浏览器使用 WebDevTools 进行代码的跟踪与调试。
iii. 与HAP数据的交互
我们提供了一个 mdye
的 npm 包来实现与HAP应用工作表数据的交互。脚手架中已经默认安装了此依赖,如果你要手动安装,可以在项目中使用如下命令安装:
$ npm i mdye --save
在项目代码中,引入 mdye
:
import { env, config, api, utils } from "mdye";
mdye
提供了4个对象:
env
用于获取视图设置项参数config
用于获取当前应用、工作表、视图相关的配置api
提供一系列的方法与HAP工作表的数据进行交互utils
调用HAP公共组件
详细用法可以查看本文档附录中的 JSSDK API 。
下面是按上述示例需求开发完成的视图插件的截图:
7. 提交本地代码
在视图插件开发过程中,可以随时将编译后的插件代码提交到服务器端保存。如果自定义视图使用了某个已提交的版本作为当前代码(需要清除本地加载的调试文件),则其他任何有该视图访问权限的人都将看到在服务端渲染的视图。而在「调试」模式下的本地地址,是只有开发者本人可以预览到视图的样式的。
提交本地代码使用如下命令:
$ mdye push -m "首次提交demo"
此时,如果开发者还没有登录授权到本地项目,则系统会弹出授权页面进行自动授权。然后,会自动编译和打包代码,并以当前登录的用户身份进行代码提交。
提交成功后,在插件的「提交」历史中可以查看到提交的代码。
8. 发布插件
视图插件在发布之前,只能在调试应用中被使用。如果想要全组织都可以使用开发好的插件,则需要将插件发布到组织。插件的发布是基于开发者提交的代码的,开发者可以选择将某次提交的代码发布为一个正式的版本。发布时,需要定义版本号,且每次发布的版本号只能大于当前的版本号。
如果管理员在「插件中心」中启用了该插件,则该插件将对全组织下的成员生效,此时所有人都可以在搭建应用时使用该视图。
此时为工作表「添加视图」时就会出现视图插件的可选项:
9. 插件管理
在「插件中心」,可以对开发中和已发布的插件进行管理。
「我开发的」列表中的插件为开发者自己创建的插件。可以在此为插件添加调试应用、查看提交历史与发布历史、发布新的插件版本。
「组织」列表中是组织下已经发布的所有插件。可以在此查看插件的发布历史,对插件进行环境参数配置,以及查看插件的使用明细。管理员也可以在此发布新版本对插件升级和回滚插件版本。
注:普通用户(非组织应用管理员)对组织下的插件只有列表查看权限,没有管理权限
10. 插件的导出导入
开发者可以在「插件中心」里将已发布的某个插件导出为一个 .mdye
文件,然后分发给其他组织或私有部署用户,后者在「插件中心」里将其导入后便可以使用该插件。
i. 导出插件
开发者在「插件中心」>「我开发的」插件列表中,可以从插件详情中的「发布历史」里点击「导出」按钮将插件导出。
开发者在导出视图插件时,通过设置授权密钥,可以对插件导入者作出如下限制:
- 设置导入密码。当导入插件时输入的密码不正确,将无法导入该插件;
- 设置授权到期时间。当导入的插件到达授权期限时,全组织内该插件都将不可用;
- 设置授权组织。当导入者所在组织不在密钥内时,将无法导入该插件;
- 设置私有部署授权服务器。当导入者所在服务器不在密钥内时,将无法导入该插件。
以上设置组合使用时,需要满足所有条件,插件才能被导入并正常使用。
导出成功后,开发者可以在「导出历史」中下载导出文件,并可以查看该文件的密钥信息:
ii. 导入插件
要导入插件时,可以在「组织」页面点击「+导入」按钮,选择一个 .mdye
文件进行导入。
如果插件设置了导出密钥,则需要输出正确的密码后才能被导入。如果插件设置了指定的组织、服务器,则在不满足环境要求时会导入失败。
导入成功后,可以在组织内启用该插件,此时组织内均可使用该插件。导入的插件不可再次导出。
在导入插件时,如果组织内已经有同源的插件,则可以选择在原有的插件上进行升级或者是创建一个新的插件(通常用于测试插件新版本):
11. 上架到插件库与安装插件
暂未开放上架到插件库功能,敬请期待。
附录一:mdye
JSSDK API
mdye.env
{
"env": {
"fields": ["controlId"], // 字段选择器
"string": "string", // 字符串
"numeric": 10, // 数值
"enum": ["key"], // 枚举值
"boolean": true, // 布尔值
}
}
mdye.config
{
"config": {
"appId": "string", // 当前应用ID
"worksheetId": "string", // 当前工作表ID
"projectId": "string", // 当前组织ID
"viewId": "string", // 当前视图ID
"filters": [{}], // 当前视图的筛选条件
"query": {}, // 当前页面的 url query 参数
"controls": [{ // 当前视图下的字段配置信息
"controlId": "string",
"controlName": "string",
......
}],
"worksheetInfo": {...} // 当前工作表配置信息
"currentAccount": { // 当前用户
"accountId": "", // 用户id
"fullname": "", // 用户名称
"avatar": "", // 用户头像
"lang": "", // 用户语言
},
}
}
mdye.api
使用 VS Code 等支持代码提示的编辑器时编辑器会自动显示使用方法
getFilterRows(params)
获取工作表行记录数据
参数:
params
:参数对象,包含以下属性:params.worksheetId
:工作表的ID,类型为字符串。params.viewId
:视图的ID。params.pageSize
:每页返回的记录数量。params.pageIndex
:要返回的页码。params.sortId
:排序字段的ID。params.isAsc
:指示排序方式是否为升序。params.notGetTotal
:当设置为true时,接口将不返回总记录数,以提高接口速度。
getFilterRowsTotalNum(params)
获取工作表行记录数
参数:
params
:参数对象,包含以下属性:params.worksheetId
:工作表的ID,类型为字符串。params.viewId
:视图的ID。params.pageSize
:每页返回的记录数量。params.pageIndex
:要返回的页码。
getRowDetail(params)
获取行记录详情
参数:
params
:参数对象,包含以下属性:params.appId
:应用ID。params.worksheetId
:工作表的ID。params.viewId
:视图的ID。params.rowId
:记录的ID。params.getTemplate
:返回对应表信息。
getRowRelationRows(params)
获取记录的关联记录 获取记录的子表记录
参数:
params
:参数对象,包含以下属性:params.controlId
:关联记录字段或子表字段的controlId。params.rowId
:记录的ID。params.worksheetId
:当前表(关联记录字段或子表字段所在表)的ID。params.keywords
:搜索记录关键字。params.pageSize
:每页数量。params.pageIndex
:页码。params.getWorksheet
:对应关联表对象。
addWorksheetRow(params)
创建记录
参数:
params
:参数对象,包含以下属性:params.appId
:应用的ID。params.worksheetId
:工作表的ID。params.receiveControls
:字段数据,具体格式可以在浏览器DevTools里看web端同名接口或更新字段数据示例 。
updateWorksheetRow(params)
更新行记录
参数:
params
:参数对象,包含以下属性:params.appId
:应用的ID。params.worksheetId
:工作表的ID。params.rowId
:记录的ID。params.newOldControl
:字段数据,具体格式可以在浏览器DevTools里看web端同名接口或更新字段数据示例 。
deleteWorksheetRow(params)
删除行记录
参数:
params
:参数对象,包含以下属性:params.appId
:应用的ID。params.worksheetId
:工作表的ID。params.rowIds
:记录ID列表。
mdye.utils
openRecordInfo(params)
打开记录详情弹窗
参数:
params
:参数对象,包含以下属性:params.appId
:应用idparams.worksheetId
:工作表idparams.viewId
:视图idparams.recordId
:记录id
返回:
返回 Promise,Promise结果是更新后的记录。
{
action: "update",
value: {
rowid: string,
[key: string]: any
}
}
openNewRecord(params)
打开创建记录弹窗
参数:
params
:参数对象,包含以下属性:params.appId
:应用idparams.worksheetId
:工作表id
返回:
返回 Promise,Promise结果是新建的记录。
{
rowid: string,
[key: string]: any
}
selectUsers(params)
选择人员
参数:
params
:参数对象,包含以下属性:params.projectId
:组织id[可选]默认当前应用所在组织params.unique
:只能选一个
返回:
返回 Promise,Promise结果是选择的人员。
[{
accountId: string,
avatar: string,
fullname: string
}]
selectDepartments(params)
选择部门
参数:
params
:参数对象,包含以下属性:params.projectId
:组织id[可选]默认当前应用所在组织params.unique
:只能选一个
返回:
返回 Promise,Promise结果是选择的部门。
[{
departmentId: string,
departmentName: string
}]
selectOrgRole(params)
选择组织角色
参数:
params
:参数对象,包含以下属性:params.projectId
:组织id[可选]默认当前应用所在组织params.unique
:只能选一个
返回:
返回 Promise,Promise结果是选择的组织。
[{
organizeId: string,
organizeName: string
}]
selectRecord(params)
选择记录
参数:
params
:参数对象,包含以下属性:params.projectId
:组织id[可选]默认当前应用所在组织params.relateSheetId
:对应的工作表 idparams.multiple
:选多个
返回:
返回 Promise,Promise结果是选择的记录。
[{
rowid: string,
[key: string]: any
}]
selectLocation(params)
选择地图定位
参数:
params
:参数对象,包含以下属性:params.distance
:距离params.defaultPosition
:默认位置 {lat, lng}params.multiple
:选多个
返回:
返回 Promise,Promise结果是选择的地点。
[{
address: string,
lat: string,
lng: string,
name: string
}]
更新字段数据示例
[
{
"controlId": "661514c080547873603db341",
"type": 2,
"value": "MINGDAO",
"controlName": "文本"
},
{
"controlId": "6615151480547873603db355",
"type": 6,
"value": "11",
"controlName": "数值"
},
{
"controlId": "6615151480547873603db356",
"type": 8,
"value": "12.00",
"controlName": "金额",
"dot": 2
},
{
"controlId": "6615151480547873603db357",
"type": 5,
"value": "hello@mingdao.com",
"controlName": "邮箱"
},
{
"controlId": "6615151480547873603db358",
"type": 15,
"value": "2024-04-10",
"controlName": "日期"
},
{
"controlId": "6615151480547873603db359",
"type": 46,
"value": "14:22:00",
"controlName": "时间"
},
{
"controlId": "6615151480547873603db35a",
"type": 3,
"value": "+8618777171711",
"controlName": "手机"
},
{
"controlId": "6615151480547873603db35c",
"type": 11,
"value": "[\"a49c9652-5551-4c3d-8fca-cb70bb009b08\"]", // 这里的key是选项字段 options 里 option 对象下的 key 属性,字段数据在 mdye.config.controls 里
"controlName": "单选"
},
{
"controlId": "6615151480547873603db35d",
"type": 10,
"value": "[\"e978414d-ccee-4cde-8e45-780d27afa8e7\",\"9ace844e-e737-4a8f-9362-96e4c83ebe91\"]", // 这里的key是选项字段 options 里 option 对象下的 key 属性,字段数据在 mdye.config.controls 里
"controlName": "多选"
},
{
"controlId": "6615151480547873603db35e",
"type": 26,
"value": "[{\"accountId\":\"60149342-0453-4c8c-bae9-6120c62ac58d\"}]",
"controlName": "成员"
},
{
"controlId": "6615151480547873603db35f",
"type": 27,
"value": "[{\"departmentId\":\"0a192f2b-7ad0-40b4-b6f8-db3f502e00a5\"}]",
"controlName": "部门"
},
{
"controlId": "6615151480547873603db360",
"type": 48,
"value": "[{\"organizeId\":\"52b067a8-696e-4e68-9473-be415cef1cd1\"}]",
"controlName": "组织角色"
},
{
"controlId": "6615151480547873603db362",
"type": 36,
"value": "1", // 选中 '1' 未选中 '0'
"controlName": "检查项"
},
{
"controlId": "6615151480547873603db363",
"type": 28,
"value": 3,
"controlName": "等级"
},
{
"controlId": "6615151480547873603db364",
"type": 41,
"value": "<h2>mingdao</h2>",
"controlName": "富文本"
},
{
"controlId": "6615151480547873603db365",
"type": 7,
"value": "321324200001010101",
"controlName": "证件"
},
{
"controlId": "6615151480547873603db366",
"type": 40,
"value": "{\"x\":124.387523,\"y\":40.123234,\"address\":\"辽宁省丹东市振兴区站前街道站后路丹东站\",\"title\":\"\"}", // x 经度 y 纬度
"controlName": "定位"
},
{
"controlId": "6615151480547873603db367",
"type": 25,
"value": "壹拾贰元",
"controlName": "大写金额"
},
{
"controlId": "6615151480547873603db368",
"type": 29,
"value": "[{\"sid\":\"df201312-5606-4d56-8342-ec2b00f9bf89\"}]", // 记录的 rowid
"controlName": "emitter"
},
{
"controlId": "6615151480547873603db36a",
"type": 35,
"value": "[{\"sid\":\"def7cd2a-d6c2-4524-bcd6-2b1df58ed6a9\"}]", // 对应级联记录的 rowid
"controlName": "级联选择"
}
]
mdye 消息系统
mdye 支持以发布订阅的模式响应插件外部的操作,比如当视图筛选发生变化时插件可以接收到变更后的筛选值去重新请求数据。
import React, { useEffect, useState, useCallback } from "react";
import { env, config, api, utils, md_emitter } from "mdye";
import { parseEnv } from "./utils";
const { getFilterRows } = api;
export default function App() {
const { appId, worksheetId, viewId, controls } = config;
const mapViewConfig = parseEnv(env);
const { loadNum } = mapViewConfig;
const [records, setRecords] = useState([]);
const [filters, setFilters] = useState({});
async function loadRecords() {
const res = await getFilterRows({
worksheetId,
viewId,
pageIndex: 1,
pageSize: loadNum,
...filters,
});
setRecords(res.data);
}
const handleFiltersUpdate = useCallback((newFilers) => {
setFilters(newFilers);
}, []);
useEffect(() => {
loadRecords();
}, [filters]);
useEffect(() => {
md_emitter.addListener("filters-update", handleFiltersUpdate);
return () => {
md_emitter.removeListener("filters-update", handleFiltersUpdate);
};
}, []);
return <div>{records.length}</div>;
}
当前支持的事件:
filters-update
筛选条件变更new-record
按钮添加记录