Skip to content

介绍

协议草案起草人

  • 撰写:赵飞虎
  • 审阅:王飞龙、帖严

版本号

0.0.1

协议版本号规范

本协议采用语义版本号,版本号格式为 major.minor.patch 的形式。

  • major 是大版本号:用于发布不向下兼容的协议格式修改
  • minor 是小版本号:用于发布向下兼容的协议功能新增
  • patch 是补丁号:用于发布向下兼容的协议问题修正

名词术语

  • 物料:能够被沉淀下来直接使用的前端能力,一般表现为业务组件、区块、模板。
  • 业务组件(Business Component):业务领域内基于基础组件之上定义的组件,可能会包含特定业务域的交互或者是业务数据,对外仅暴露可配置的属性。(low-code 有,现有系统没有)
  • 区块(Block**)**:通过低代码搭建的方式,将一系列业务组件、布局组件进行嵌套组合而成,不对外提供可配置的属性。可通过区块容器组件的包裹,实现区块内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可通过复制 schema 实现跨页面、跨应用的快速复用,保障功能和数据的正常。(low-code 有,现有系统没有,后期可基于现有物料体系添加)
  • 模板(Template):特定垂直业务领域内的业务组件、区块可组合为单个页面,统称为模板。

物料规范定义

  • 源码物料规范:一套面向开发者的目录规范,用于规范化约束开发过程中的代码、文档、接口规范,以方便物料集成到低代码平台。
  • 搭建物料规范:一套面向开发者的 Schema 规范,用于规范化约束开发过程中的代码、文档、接口规范,以方便物料集成到低代码平台。

物料规范 - 业务组件规范

源码规范

目录规范

容器组件对应两个状态:设计期状态可以接收组件拖拽,运行期状态负责组件渲染,所以需要编写两个SFC文件,命名规则需严格遵守:容器名称-widget(设计期)、容器名称-item(运行期); 字段组件在设计期和运行期共用,故只需要编写一个SFC文件,命名规则需严格遵守:组件名称-widget。

component                      //  组件名称, 比如 card
|—— component-widget.vue			 // 容器组件对应两个状态:设计期状态可以接收组件拖拽,
|—— component-item.vue         // 运行期状态负责组件渲染
|—— index.js									 // 抛出 Vue 安装插件
|—— propsConfig.vue            // 自定义属性设置组件(非必须)
|—— schema.js 								 // 抛出一个 JSON Schema 对象
|—— sfc-generator.js           // 抛出一个组件代码生成器函数
|—— README.md									 // 物料说明文件
|—— package.json							 // 组件 package.json
|—— en-US.js									 // 国际化(英语)
|—— zh-CN.js                   // 国际化(中文)
|—— component.svg              // 物料库图标
  • component-widget.vue 设计状态的组件,可以接收组件拖拽,示例代码:
vue
<!--
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-09-13 10:03:30
 * @LastEditTime: 2024-03-07 11:39:16
-->
<script>
import { ObjectUtils } from 'motl'
import { IkingPicker } from 'iking-web-ui'

import { emitter, fieldMixin, FormItemWrapper, i18n } from '@/utils/iking-form-extension'

/**
 * 构造需要透传给组件的默认属性
 */
const defaultProps = () => {
  return {
    chooseType: ['user'],
    tabs: ['group', 'role', 'post'],
    typeOption: {
      dep: 'DEPT',
      user: 'USER',
      role: 'ROLE',
      post: 'POST'
    },
    propOption: { name: 'elementName', id: 'elementId', type: 'elementType' }
  }
}

export default {
  // 物料名称
  name: 'IkPickerWidget',
  // 必须固定为FieldWidget,用于接收父级组件的broadcast事件
  componentName: 'FieldWidget',
  components: {
    // 所有表单项组件都需要在跟元素外面加上这个包装组件
    FormItemWrapper,
    IkingPicker
  }, 
  mixins: [emitter, fieldMixin, i18n],
  inject: ['getFormConfig'],
  props: {
    // 此物料对应的 schema 对象(此变量名不能改变、删除)
    field: Object,
    // 此物料的父组件 schema 对象(此变量名不能改变、删除)
    parentWidget: Object,
    // 父物料的子物料列表(此变量名不能改变、删除)
    parentList: Array,
    // 此物料在 parentList 中的索引(此变量名不能改变、删除)
    indexOfParentList: Number,
    // 全局保存设时信息的对象(此变量名不能改变、删除)
    designer: Object,
    // 当前是否处于设计时状态(此变量名不能改变、删除)
    designState: {
      type: Boolean,
      default: false
    },
    /** 子表单相关信息(此变量名不能改变、删除)  begin */
    subFormRowIndex: {
      /* 子表单组件行索引,从0开始计数 */ type: Number,
      default: -1
    },
    subFormColIndex: {
      /* 子表单组件列索引,从0开始计数 */ type: Number,
      default: -1
    },
    subFormRowId: {
      /* 子表单组件行Id,唯一id且不可变 */ type: String,
      default: ''
    }
    /** 子表单相关信息(此变量名不能改变、删除)  end */
  },
  data() {
    return {
      /** 通用属性(此变量名不能改变、删除) begin */
      oldFieldValue: null, // field组件change之前的值(此变量名不能改变)
      rules: [], // 表单校验规则(此变量名不能改变)
      fieldModel: [], // 组件双向绑定的字段(此变量名不能改变)
      /** 通用属性(此变量名不能改变、删除) end */
      // 透传给组件的属性
      propsModel: defaultProps(),
      pickerVisible: false
    }
  },
  computed: {
    api() {
      const headers = {
        'Content-Type': 'application/json;charset=UTF-8'
      }
      for (const header of this.page.globalDatasourceHeaders ?? [])
        headers[header.name] = header.value

      return {
        methods: 'post',
        url: `${this.getFormConfig()?.dataSourceHost}/server/component/pick/mix`,
        headers
      }
    },
    label() {
      return (this.fieldModel ?? []).map(it => it.elementName).join(';')
    }
  },
  watch: {
    fieldModel(val) {
      this.handleChangeEvent(val)
    }
  },
  created() {
    /* 注意:子组件mounted在父组件created之后、父组件mounted之前触发,故子组件mounted需要用到的prop
         需要在父组件created中初始化!! */
         
    /** 通用初始化 begin */
    this.init(); // (此变操作不能改变、删除)
    // 初始化需要透传给
    this.initPropsModel()
    /** 通用初始化 end */
    this.initApiHeaders()
  },

  mounted() {
    this.handleOnMounted()
  },

  beforeUnmount() {
    this.unregisterFromRefList()
  },

  methods: {

    // 通用表单字段物料初始化(所有表单字段初始化都需要调用的方法)
    init(){
      this.registerToRefList()
      this.initFieldModel()
      this.initEventHandler()
      this.buildFieldRules()
      this.handleOnCreated()
    },

    initApiHeaders() {
      // 设置全局请求头
      for (const header of this.page.globalDatasourceHeaders ?? [])
        this.api.headers[header.name] = header.value
    },


    initPropsModel() {
      ObjectUtils.copyValue(this.propsModel, this.field?.options)
    },
    openShowPicker() {
      if (this.designState)
        return

      this.pickerVisible = true
    },
    handleSelected(val) {
      
    }
  }
}
</script>

<template>
  <FormItemWrapper
    :designer="designer"
    :field="field"
    :design-state="designState"
    :parent-widget="parentWidget"
    :parent-list="parentList"
    :index-of-parent-list="indexOfParentList"
    :sub-form-row-index="subFormRowIndex"
    :sub-form-col-index="subFormColIndex"
    :sub-form-row-id="subFormRowId"
  >
    <div class="ik-picker">
      <el-input
        v-model="label"
        readonly
        :clearable="field.options.clearable"
        :placeholder="field.options.placeholder"
        @click="openShowPicker"
      />
      <IkingPicker
        ref="refIkingPicker"
        v-bind="propsModel"
        v-model="fieldModel"
        v-model:show="pickerVisible"
        :api="api"
        @ok="handleSelected"
      />
    </div>
  </FormItemWrapper>
</template>

<style lang="scss" scoped>
.ik-picker {
  width: 100%;
}
</style>
  • component-item.vue 运行期状态的组件,示例代码:
vue
<!--
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-09-13 10:03:30
 * @LastEditTime: 2024-03-07 11:39:16
-->
<script>
import { ObjectUtils } from 'motl'
import { IkingPicker } from 'iking-web-ui'

import { emitter, fieldMixin, FormItemWrapper, i18n } from '@/utils/iking-form-extension'

/**
 * 构造需要透传给组件的默认属性
 */
const defaultProps = () => {
  return {
    chooseType: ['user'],
    tabs: ['group', 'role', 'post'],
    typeOption: {
      dep: 'DEPT',
      user: 'USER',
      role: 'ROLE',
      post: 'POST'
    },
    propOption: { name: 'elementName', id: 'elementId', type: 'elementType' }
  }
}

export default {
  // 物料名称
  name: 'IkPickerWidget',
  // 必须固定为FieldWidget,用于接收父级组件的broadcast事件
  componentName: 'FieldWidget',
  components: {
    // 所有表单项组件都需要在跟元素外面加上这个包装组件
    FormItemWrapper,
    IkingPicker
  }, 
  mixins: [emitter, fieldMixin, i18n],
  inject: ['getFormConfig'],
  props: {
    // 此物料对应的 schema 对象(此变量名不能改变、删除)
    field: Object,
    // 此物料的父组件 schema 对象(此变量名不能改变、删除)
    parentWidget: Object,
    // 父物料的子物料列表(此变量名不能改变、删除)
    parentList: Array,
    // 此物料在 parentList 中的索引(此变量名不能改变、删除)
    indexOfParentList: Number,
    // 全局保存设时信息的对象(此变量名不能改变、删除)
    designer: Object,
    // 当前是否处于设计时状态(此变量名不能改变、删除)
    designState: {
      type: Boolean,
      default: false
    },
    /** 子表单相关信息(此变量名不能改变、删除)  begin */
    subFormRowIndex: {
      /* 子表单组件行索引,从0开始计数 */ type: Number,
      default: -1
    },
    subFormColIndex: {
      /* 子表单组件列索引,从0开始计数 */ type: Number,
      default: -1
    },
    subFormRowId: {
      /* 子表单组件行Id,唯一id且不可变 */ type: String,
      default: ''
    }
    /** 子表单相关信息(此变量名不能改变、删除)  end */
  },
  data() {
    return {
      /** 通用属性(此变量名不能改变、删除) begin */
      oldFieldValue: null, // field组件change之前的值(此变量名不能改变)
      rules: [], // 表单校验规则(此变量名不能改变)
      fieldModel: [], // 组件双向绑定的字段(此变量名不能改变)
      /** 通用属性(此变量名不能改变、删除) end */
      // 透传给组件的属性
      propsModel: defaultProps(),
      pickerVisible: false
    }
  },
  computed: {
    api() {
      const headers = {
        'Content-Type': 'application/json;charset=UTF-8'
      }
      for (const header of this.page.globalDatasourceHeaders ?? [])
        headers[header.name] = header.value

      return {
        methods: 'post',
        url: `${this.getFormConfig()?.dataSourceHost}/server/component/pick/mix`,
        headers
      }
    },
    label() {
      return (this.fieldModel ?? []).map(it => it.elementName).join(';')
    }
  },
  watch: {
    fieldModel(val) {
      this.handleChangeEvent(val)
    }
  },
  created() {
    /* 注意:子组件mounted在父组件created之后、父组件mounted之前触发,故子组件mounted需要用到的prop
         需要在父组件created中初始化!! */
         
    /** 通用初始化 begin */
    this.init(); // (此变操作不能改变、删除)
    // 初始化需要透传给
    this.initPropsModel()
    /** 通用初始化 end */
    this.initApiHeaders()
  },

  mounted() {
    this.handleOnMounted()
  },

  beforeUnmount() {
    this.unregisterFromRefList()
  },

  methods: {

    // 通用表单字段物料初始化(所有表单字段初始化都需要调用的方法)
    init(){
      this.registerToRefList()
      this.initFieldModel()
      this.initEventHandler()
      this.buildFieldRules()
      this.handleOnCreated()
    },

    initApiHeaders() {
      // 设置全局请求头
      for (const header of this.page.globalDatasourceHeaders ?? [])
        this.api.headers[header.name] = header.value
    },


    initPropsModel() {
      ObjectUtils.copyValue(this.propsModel, this.field?.options)
    },
    openShowPicker() {
      if (this.designState)
        return

      this.pickerVisible = true
    },
    handleSelected(val) {
      
    }
  }
}
</script>

<template>
  <FormItemWrapper
    :designer="designer"
    :field="field"
    :design-state="designState"
    :parent-widget="parentWidget"
    :parent-list="parentList"
    :index-of-parent-list="indexOfParentList"
    :sub-form-row-index="subFormRowIndex"
    :sub-form-col-index="subFormColIndex"
    :sub-form-row-id="subFormRowId"
  >
    <div class="ik-picker">
      <el-input
        v-model="label"
        readonly
        :clearable="field.options.clearable"
        :placeholder="field.options.placeholder"
        @click="openShowPicker"
      />
      <IkingPicker
        ref="refIkingPicker"
        v-bind="propsModel"
        v-model="fieldModel"
        v-model:show="pickerVisible"
        :api="api"
        @ok="handleSelected"
      />
    </div>
  </FormItemWrapper>
</template>

<style lang="scss" scoped>
.ik-picker {
  width: 100%;
}
</style>
  • index.js 抛出 Vue 安装插件,示例代码:
javascript
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-09-13 11:56:07
 * @LastEditTime: 2024-03-21 09:34:21
 */
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-08-22 17:43:00
 * @LastEditTime: 2023-09-13 11:56:33
 */
import schema from './schema';
import widget from './ik-picker-widget.vue';
import item from './ik-picker-widget.vue';
import langZH from './zh-CN';
/**
 * 加载扩展物料
 *
 * @param {*} app Vue 应用实例
 * @param {*} loadExtension 加载扩展物料函数
 */
export const load = function (app, loadExtension) {
  const options = {
    schema: schema,
    designTime: widget,
    runtime: item,
    group: 'advance',
    langZH,
    propConfigs: [
      {
        type: 'InputText',
        uniquePropName: 'ik-picker-placeholder',
        propEditorName: 'ik-picker-placeholder-editor',
        propName: 'placeholder',
        propLabelKey: 'extensions.ikPicker.options.placeholder',
      },
    ],
  };
  loadExtension(app, options);
};

export default {
  install(app, loadExtension) {
    load(app, loadExtension);
  },
};
  • propsConfig.vue 自定义属性设置组件,示例代码如下:
vue
<template>
  <el-form-item label="占位提示信息">
    <el-input v-model="optionModel.placeholder" />
  </el-form-item>
</template>
<script setup>
  const props = defineProps({
    designer: Object,
    selectedWidget: Object,
    optionModel: Object,
  });


  console.log('props ', props);
</script>
  • schema.js:抛出一个 JSON Schema 对象,示例代码:
javascript
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-09-15 17:21:18
 * @LastEditTime: 2024-02-28 14:16:45
 */
import { commonFieldOptions } from '@/utils/iking-form-extension';
export default {
  type: 'ik-picker',
  formItemFlag: true,
  icon: 'ik-picker',
  events: [{ name: 'onChange', label: '改变', description: '组件数据(v-model)改变时触发' }],
  methods: [],

  options: {
    /** 通用选项 begin */
    // name: '',
    keyNameEnabled: false,
    keyName: '', //数据键值名称
    label: '人员选择器',
    customClass: '',
    defaultValue: [],
    placeholder: '',
    // 基础组件通用选项 begin
    ...commonFieldOptions(),
    // 基础组件通用选项 end
    /** 通用选项 end */

    /** 事件 begin */
    onChange: [],
    /** 事件 end */
  },
};
  • sfc-generator.js 抛出一个组件代码生成器函数,示例代码:
javascript
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-08-24 14:21:03
 * @LastEditTime: 2023-08-24 14:25:13
 */
export default function (cw, formConfig, vue3Flag = false, {buildClassAttr, buildContainerWidget, buildFieldWidget}) {
  const wop = cw.options
  const classAttr = buildClassAttr(cw)
  const styleAttr = !!wop.cardWidth ? `style="{width: ${wop.cardWidth} !important}"` : ''
  const shadowAttr = `shadow="${wop.shadow}"`
  const vShowAttr = !!wop.hidden ? `v-show="false"` : ''

  const cardTemplate =
`<div class="card-container">
  <el-card ${classAttr} ${styleAttr} ${shadowAttr} ${vShowAttr}>
    <template #header>
      <div class="clear-fix">
        <span>${wop.label}</span>
        ${!!wop.showFold ? `<i class="float-right el-icon-arrow-down"></i>` : ''}
      </div>
    </template>
    ${
      cw.widgetList.map(wItem => {
        if (wItem.category === 'container') {
          return buildContainerWidget(wItem, formConfig, vue3Flag)
        } else {
          return buildFieldWidget(wItem, formConfig, vue3Flag)
        }
      }).join('')
    }
  </el-card>
</div>`
  return cardTemplate
}
  • README.md 应该包含业务组件的源信息、使用说明以及 API
  • package.json 组件 package.json
json
{
  "name": "@iking-page/ik-picker",
  "description": "人员选择",
  "version": "0.0.1",
  "main": "index.js"
}
  • en-US.js 国际化(英语)
  • zh-CN.js 国际化(中文),示例代码:
javascript
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2023-09-13 11:45:55
 * @LastEditTime: 2023-11-21 17:15:06
 */
export default {
  type: 'ik-picker',
  name: 'ikPicker',
  label: '人员',
  options: {
    url: '数据请求url',
    placeholder: '占位内容',
  },
};
  • component.svg 物料库图标

API 规范

API 是组件的属性解释,给开发者作为组件属性配置的参考。为了保持 API 的一致性,我们制定这个 API 命名规范。对于业界通用的,约定俗成的命名,我们遵循社区的约定。对于业界有多种规则难以确定的,我们确定其中一种,大家共同遵守。

通用命名

API 名称类型描述常见变量
shapestring形状,从组件的外形来看有区别的时候,使用 shape
directionenum方向,取值采用缩写的方式。hoz(水平), ver(垂直)
alignenum对齐方式tl, tc, tr, cl, cc, cr, bl, bc, br
statusenum状态normal, success, error, warning
sizeenum大小small, medium, large 更大或更小可用 (xxs, xs, xl, xxl)
typeenum or string分类:1. dom 结构不变、只有皮肤的变化 2.组件类型只有并列的几类normal, primary, secondary
visibleboolean是否显示
defaultVisibleboolean是否显示(非受控)
disabledboolean禁用组件
closablebool/string允许关闭的方式
htmlTypestring当原生组件与 Fusion 组件的 type 产生冲突时,原生组件使用 htmlType
linkstring链接
dataSourcearray列表数据源[{label, value}, {label, value}]
has+'属性'boolean拥有某个属性例如 hasArrow, hasHeader, hasClose 等等

事件

  • 标准事件或者自定义的符合 w3c 标准的事件,命名必须 on 开头, 即 on + 事件名,如 onExpand。

开发指南

  1. iking-form 抛出了创建物料以及向物料面板注册物料所需要的 api,该api 在 iking-form在iking-form 默认导出的对象的 extensionSDK 属性上,在开发时可以引用该api 进行开发。可以选择将其导出为一个utils文件方便使用,如下所示:
  • @/utils/iking-form-extension.js
javascript
/*
 * @Author: qiye
 * @LastEditors: qiye
 * @description: page description
 * @Date: 2024-02-28 13:22:47
 * @LastEditTime: 2024-02-29 15:23:50
 */
import ikingformSDK from 'mo-form'

const extensionSDK = ikingformSDK.ExtensionSDK

const {
  StaticContentWrapper,
  emitter,
  i18n,
  fieldMixin,
  containerMixin,
  ContainerWrapper,
  FieldComponents,
  refMixinDesign,
  ContainerItemWrapper,
  containerItemMixin,
  refMixin,
  FormItemWrapper,
  SvgIcon,
  CodeEditor,
  firstToUpperCase,
  findFormList,
  commonFieldOptions,
  useMessage,
  findWidgetById,
  setGlobalApp,
  register,
  getDSByName,
  runDataSourceRequest,
  getAllFieldWidgets,
  deepClone,
  traverseFieldWidgetsOfContainer,
  getFieldWidgetByName,
  hasPropertyOfObject,
  setObjectValue,
  getObjectValue,
  registerExtensionZH,
  generateId
} = extensionSDK

export {
  StaticContentWrapper,
  emitter,
  i18n,
  fieldMixin,
  containerMixin,
  ContainerWrapper,
  FieldComponents,
  refMixinDesign,
  ContainerItemWrapper,
  containerItemMixin,
  refMixin,
  FormItemWrapper,
  SvgIcon,
  CodeEditor,
  firstToUpperCase,
  findFormList,
  commonFieldOptions,
  useMessage,
  findWidgetById,
  setGlobalApp,
  register,
  getDSByName,
  runDataSourceRequest,
  getAllFieldWidgets,
  deepClone,
  traverseFieldWidgetsOfContainer,
  getFieldWidgetByName,
  hasPropertyOfObject,
  setObjectValue,
  getObjectValue,
  registerExtensionZH,
  generateId
}
  1. 在项目的入口文件main.ts中引入 setGlobalAppregister方法,用于注册物料到 iking-form的面板中,如下所示:
javascript
import {setGlobalApp, register} from '@/utils/iking-form-extension';
  1. 将物料注册到物料面板中:
javascript

const app = createApp(App)
/** 第三方扩展 begin */
setGlobalApp(app);
/** 第三方扩展 demo begin */
// 物料目录在extension目录下,比如ik-picker物料的目录为:
// @/extension/ik-picker
const demoModules: any = import.meta.glob('./extension/*/index.js', {eager: true})
for (const path in demoModules) {
  register(demoModules[path].default)
}
/** 第三方扩展 demo end */