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中引入 setGlobalApp、register方法,用于注册物料到 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 */