低代码在EHR领域中的实践 - 表单设计

2,489 阅读15分钟

在上一篇文章《低代码在EHR领域中的实践 - 架构设计与规划》介绍了飞轮低代码采取了模型驱动的设计理念,通过一套实体模型产出不同的信息集,表单是其中之一,也是飞轮低代码平台能够对业务产生价值的核心支持。飞轮低代码表单的落地离不开服务端的配合,接下来跟大家分享下我们前后端共同沉淀的飞轮低代码表单设计方案。

架构设计

飞轮低代码表单基于element-ui开发,主要通过一套标准化的JSON Schema协议来实现UI渲染数据填充规则校验联动逻辑动作配置等核心功能。

前端技术架构图.png

飞轮低代码表单架构可以这么理解:

  • 规范层:通过可视化编辑器进行表单配置,服务端根据配置信息产出【表单描述协议】、【UI描述协议】、【逻辑描述协议】、【表单动作协议】,供渲染层消费。
  • 渲染层:通过接口获取协议信息并进行解析,协议按照约定的规则进行渲染。表单渲染具有一套特殊的生命周期:
    • 加载时:解析表单属性和组件属性,将获取的数据处理成组件所需格式;对字段逻辑做初步解析,解析字段逻辑的条件依赖,在条件依赖上也挂载逻辑(见下文解释);处理只读,即在查看页面状态下,将表单组件转成文本形式。
    • 渲染时:触发配置的加载时的字段逻辑,并根据解析好的数据进行组件渲染,同时,用户变更字段触发变更后的逻辑。
    • 保存时:执行保存前的字段逻辑,将数据处理成服务端需要的格式;如果有信息集动作配置,则执行信息集动作配置(见下文解释)。
  • 组件层:表单采取了渲染和组件(通用/定制化)分离管理的模式,表单只负责渲染的工作,可以在不影响表单核心渲染功能的情况下,对表单组件进行扩展。
  • 业务层:表单内部对新建/编辑/查看形态进行了兼容,通过一个标志可以使承载表单的页面表现出多种页面形态,而不需要针对不同形态的页面重复配置多套表单。

协议设计

表单整体的JSON Schema协议设计如下图所示:

JSON Schema (2).png

对应转换的JSON格式如下:

{
  // 表单描述协议
  module: {
    name: '表单',
    showName: false,
    ...
  },
  // 字段分组
  group: [
    { 
      name: "默认分组",
      fieldList: [
        // 字段描述协议 
        { 
          fieldName: '工号',
          value: 'H1111',
          component: 'ease-text',
          ...
        },
        ...
      ],
    }
  ],
  // 表单动作协议
  handle: [
    { 
      type: '2', // 保存前/保存后/删除前/删除后
      url: '/api' // 动作触发接口
    },
    ...
  ]
}

整体的JSON Schema协议主要由三部分组成:

  • module:表单描述协议
  • group: 字段分组协议
  • handle:表单动作协议

表单描述协议

表单描述协议用来描述表单的一些基本信息,包含表单标题、是否显示标题、表单属性等。其中,表单属性中支持了是否行内表单、表单域标签位置、表单域标签宽度等el-form属性,可以通过可视化编辑器对这些表单属性进行修改。

// 表单描述协议
module: {
  name: '表单',
  showName: true,
  property: { // 表单属性
    inline: false,
    labelPosition: 'right',
    labelWidth: '140',
    ...
  }
  ...
},

表单动作协议

表单动作协议又称为信息集动作协议(信息集,实体字段的集合,通过集合的字段渲染成表单), 表单的保存有三个阶段:保存前 => 保存中(入库)=> 保存后。飞轮提供通用的表单保存接口,保存前和保存后根据业务需要可在表单动作协议中定义。目前,在EHR系统中,保存前一般用来做数据校验,保存后一般用来做数据同步。

比如,在EHR系统中办理离职业务。离职信息保存后,分别需要往岗位变化表和离职记录表中插入记录,同时更新工作信息表中的在职状态。将这些逻辑集成在新的接口里,离职表单信息保存成功后调用动作协议中配置的接口去执行这些业务逻辑。

字段分组协议

分组协议包含分组名称和分组字段,使实体字段在页面中可分组展示。如图所示:

image.png

其中,最核心的为分组中的【字段描述协议】,对应的JSON格式如下:

{
  fieldCode: "",     // 字段编码
  fieldName: "",     // 字段名
  component: "",     // 控件名
  isRequired: false, // 是否必填
  isShow: true,      // 是否显示
  editable: true,    // 是否可编辑
  optionUrl: null,   // 下拉URL 
  options: null,     // 下拉选
  value: "",         // 值
  linkage:[],        // 字段逻辑
  ...
}

字段描述协议又分两部分组成:

  • UI描述协议:除linkage以外的其余属性
  • 逻辑描述协议:linkage属性

UI 描述协议

UI描述协议】中的字段名、控件类型、控件属性、校验信息配置都与业界大部分低代码表单配置基本一致,这里就不再赘述,主要聊聊亮点和差异。

  1. 是否可见(isShow)、是否可编辑(editable)属性

在飞轮低代码的设计中,这两个属性在配置上都会区分新建/编辑页,也就是说可视化编辑器中会针对这两个属性提供不同页面状态下的配置。

比如,是否可见属性会有【新建页是否可见】、【编辑页是否可见】两个配置。在业务层,前端只需要告诉服务端页面状态参数,服务端会将相应页面状态下配置的是否可见属性映射给isShow属性,页面渲染时只需要根据isShow属性来控制字段显隐即可。

这种设计能让配置者通过勾选的方式在不同页面的状态下快速配置字段,来满足差异化的业务需求。

image.png

  1. options下拉选项

对于下拉性质的组件,通常会需要配置下拉选项(options),下拉选项通过关联字典的形式完成配置。关联字典来自于飞轮低代码数据字典管理模块,该模块统一管理了系统中所有的字典项,方便对全系统字典进行管理、维护和复用。 字典项信息中除了有基本的labelvalue外,还有排序、是否有效两个属性来满足业务需要。

  1. optionUrl下拉url

字段的下拉选项不一定是固定的字典项,大多数情况下需要通过url去获取,例如,省市区选项。在传统的开发方式中,通常是由服务端通过代码方式提供接口,而在飞轮低代码平台中可以通过接口生成器将编码、类型、sql等配置自动化生成接口,来自定义查询逻辑。

image.png

目前飞轮EHR系统中有100多个接口通过配置化的方式来获取下拉选项。

最后,表单解析props、处理optionsUI属性,通过渲染函数来完成,核心代码:

export default {
  watch: {
    'item.optionUrl': {
      // ...请求&&处理数据
    }
  },
  render(h) {
    // ....处理属性props
    // 渲染
    return h(Comp, {
      props: {
        ...props
      }
    })
  }
}

逻辑描述协议

【逻辑描述协议】运用在字段逻辑上,可以说是表单JSON Schema设计中最核心的部分。经过一年多的业务实践,逻辑描述协议基本稳定并且满足了现有EHR业务的所有需求。

通过可视化编辑器对字段进行逻辑配置后,服务端将逻辑配置保存在数据库中供数据导入的时候执行,并产出如下格式的逻辑描述协议供前端使用:

{
  linkage: [
    {
      triggerType: '', // 触发类型
      exeConditions: '', // 触发时机
      fields: [
        { // 条件配置
          relativeCode: "", // 字段code
          operate: "",  // 关系符:包括等于、不等于、包含、不包含等
          value: "" // 值
        }
      ],
      exp: '', // 条件关系
      actions: [
        { // 动作配置
          opType: "",
          url: "",
          which: [
            {
              code: "",
              operate: '',
              value: ""
            }
          ]
        }
      ]
    }
  ]
}

逻辑描述协议中有几个关键属性:

  • triggerType:触发类型,即字段逻辑在什么时机下触发,包括【加载时】、【变更后】、【保存前】三个状态。
  • exeConditions:触发时机,即字段逻辑在什么场景下触发,包括【新增页】、【编辑页】、【新增导入】、【编辑导入】,默认全覆盖,可以根据业务场景进行选择。
  • fields:条件配置,即字段逻辑触发的条件,可以是字段本身,由【字段】、【关系符】、【值】组成,比如证件类型 - 等于 - 身份证。条件可以有多个。
  • exp:条件关系,即字段逻辑满足多个条件的逻辑运算结果才触发,包括【且】、【或】、【非】和优先级【()】,比如条件 1(且)条件 2(或)条件 3。
  • actions:动作配置,即字段逻辑产生什么动作,其中:
    • opType:动作类型,目前有11种动作类型,具体见下文;
    • url:应用于动态赋值、动态校验等动作类型;
    • which:指定产生影响的字段;

在一个逻辑定义中,同一个条件下,可以定义多种动作类型,一个动作类型下可以配置多个需要影响的字段。

以上可以简单描述为:字段在什么场景的什么时机下,当满足什么条件时,执行什么动作

联动场景

逻辑描述协议基本覆盖了一对一联动、一对多联动、多对一联动、链式联动等联动场景。联动在实现方式上有两种:

  1. 通过字段change触发逻辑
  2. 通过字段watch去反向监听

两种方式在配置上各有优势,前者配置一对多的逻辑更有优势,后者能更快配置多对一的逻辑。我们选用的是第一种方式,这种方式在多对一的场景下,多个字段影响一个字段,需要在每个依赖的字段上配置变更逻辑,这无形之间给配置带来麻烦。我们的解法是,将配置的条件(多个影响因素)作为依赖,在页面加载时去收集这些依赖,在依赖改变时自动触发逻辑

联动类型

联动类型可以分为静态联动和动态联动两类。

  • 静态联动

即在不需要与服务端数据交互下产生的联动,包括显示隐藏、是否必填、是否可编辑、静态赋值、静态下拉选项、静态检查、日期范围控制等。下面通过一个场景介绍静态联动:

场景:当证件类型等于身份证时,性别不可编辑

1.gif

配置如下:

{
  linkage: [
    {
      triggerType: '1', // 字段变更后
      exeConditions: 'insert,update', // 在新增/编辑页触发
      fields: [
        {
          relativeCode: 'F00720', // 证件类型字段 code
          operate: '10',  // 等于
          value: '1' // 身份证
        }
      ],
      exp: '1',
      actions: [
        {
          id: null,
          opType: '4', // 是否可编辑
          url: null,
          which: [
            {
              code: 'F00370', // 性别字段 code
              operate: null,
              value: false // 编辑属性值 false
            }
          ]
        }
      ]
    }
  ]
}

在上面的协议中,fields定义了【当证件类型等于身份证】条件,exp使该条件生效(这里只有一个条件,所以配置1),actions描述的是可编辑变更动作,即【字段变更时将性别的可编辑属性设置成false】。同理,如果想控制显示隐藏、是否必填、字段静态赋值等,只需配置对应的动作类型opType。

此外,在代码中,对于显示隐藏、是否必填、是否可编辑这种非是即否的逻辑,在逻辑执行时,不满足条件的情况下会自动取反。

核心代码:

// 处理条件
handleExp('条件fields', '表达式exp')
// 动作:条件满足时,执行逻辑。改变哪个字段,哪个属性,赋什么值
transfor('哪个字段', 'editable(要改变的属性)', '=(关系符)', 'false(结果)')
  • 动态联动

即需要与服务端交互产生的联动,通常需要通过接口去获取结果,包括动态赋值和动态校验等。下面通过一个场景介绍动态联动:

场景:当证件类型等于身份证时,根据身份证信息计算性别。

动态联动.gif

配置如下:

{
  linkage: [
    {
      triggerType: '1', // 字段变更后
      exeConditions: 'insert,update', // 在新增/编辑页触发
      fields: [
        {
          relativeCode: 'F00720', // 证件类型字段 code
          operate: '10',  // 等于
          value: '1' // 身份证
        }
      ],
      exp: '1',
      actions: [
        {
          id: '1474213865556626082', // 公式 id
          opType: '1', // 动态赋值
          url: '/flux/calculator/result',
          which: [
            {
              code: 'F00370', // 性别字段 code
              operate: null,
              value: 'F00370' // 匹配接口忠放回的 F00370 值
            }
          ]
        }
      ]
    }
  ]
}

在上面的协议中,fields定义了【当证件类型等于身份证】条件,exp即条件表达式(这里只有一个条件,所以配置1),actions描述的是动态赋值动作,即【通过接口/flux/calculator/result获取数据,将返回数据中的F00370字段值,给性别字段赋值】。

动态联动主要是通过公式编辑器对运算逻辑进行编写,公式编辑器忠集成了运算符、信息集字段、环境变量、常用函数(目前沉淀了大约100多个)等配置,在编辑器中编写可在java环境下运行的groovy脚本,服务端将配置信息和groovy脚本解析之后转化为接口供计算调用。

image (1).png

核心代码:

// 处理条件
handleExp('条件fields', '表达式exp')
// 条件满足时,执行逻辑
asyncTransform () {
  // 请求接口
  const res = await this.handleRequest(url, params)
  // 执行逻辑
  transfor('哪个字段', 'value(属性)', '=(关系符)', 'xxx(结果)')
}

规则校验

字段的规则校验分为静态校验和动态校验,将不同来源的字段校验处理在rules中,最后作为form-itemprops属性。

  • 静态校验

静态校验包括必填属性校验、长度校验、正则校验。通过配置中的属性进行对应的规则转换。

  • 动态校验

即需要与服务端交互的校验,采用上文提到的动态联动的配置方式,通过公式编辑器的方式转成可供前端调动的接口。下面通过一个场景介绍动态校验:

动态校验.gif

在前端渲染时,会获取动作类型为动态校验的字段逻辑,通过validator进行异步校验,核心代码:

let asyncValid = {
  trigger,
  async validator (value, rule, callback) {
    // 可以配置多个异步校验
    const resArr = await Promise.all(valid.map((val) => {
      return that.$post(val.url, payload)
    }))
    let message = ''
    // 处理错误信息
    resArr.forEach((res) => { message += res.message ? `${res.message};` : '' })
    return message ? callback(new Error(`${message}`)) : callback()
  }
}
rules.push(asyncValid)

动态下拉

即联动下拉,是特殊的联动类型。比如根据不同的【离职类型】获取不同的【离职原因分类】选项,根据不同的【离职原因分类】获取不同的【离职具体原因】选项,如下所示:

下拉联动.gif

针对这些场景,我们在配置上的设计比较简单,通过在实体的optionUrl上反向挂载依赖的方式实现,比如:

'xxx/parent_code_id={离职类型}' // 离职原因分类的 optionUrl 配置
'xxx/parent_code_id={离职原因分类}' // 离职具体原因的 optionUrl 配置

逻辑可视化

通过配置化方式虽然方便快捷,但配置的过程也容易导致许多问题。如字段逻辑没有效果,不正确配置导致页面卡顿等等,特别是在字段多、逻辑复杂的页面,很难定位到问题,错误配置也会影响功能的稳定性。针对以上问题,我们将逻辑变成可视化。

  • 表单字段逻辑可视化

根据配置信息,将字段逻辑转化成更直观的方式。如果配置上没达到预期效果,可以通过可视化快速定位问题,同时,更直观掌握该模块的业务逻辑,方便后期产品的迭代。这也是传统开发方式导致难以维护解决方式之一。

  • 错误信息可视化

错误的配置,这部分错误没有统一的规则,不能在配置时进行拦截。飞轮初期,在不熟悉配置的情况下经常会有配置错误问题,将常见的错误配置,以可视化的形式展示,减少人工排错,同时,可以提高配置质量。

image.png

结尾

当我们见过了琳琅满目的表单设计,每种方案最后是否真正满足需求、是否整体提高了效率、是否真正解放了前端,是我们应该思考的问题~最终,我们调研了业界产品并结合自身业务需求,开发出了飞轮低代码表单。

飞轮低代码表单是我们前后端协同设计的成果,目前在飞轮EHR系统中,产品和后端同学配置完成了50+个表单页100+个字段逻辑,覆盖了100%的表单业务场景,在表单业务中完全释放了前端。

这次将我们的飞轮低代码表单的设计理念做个总结输出,希望能给大家在做表单开发的时候有个参考帮助,也希望能与大家一起探讨,听取一些改进的建议。

最后,祝各位掘友们新年快乐!!

相关资料

Formily