介绍
协议草案起草人
- 撰写:赵飞虎
- 审阅:王飞龙、帖严
版本号
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 名称 | 类型 | 描述 | 常见变量 |
---|---|---|---|
shape | string | 形状,从组件的外形来看有区别的时候,使用 shape | |
direction | enum | 方向,取值采用缩写的方式。 | hoz(水平), ver(垂直) |
align | enum | 对齐方式 | tl, tc, tr, cl, cc, cr, bl, bc, br |
status | enum | 状态 | normal, success, error, warning |
size | enum | 大小 | small, medium, large 更大或更小可用 (xxs, xs, xl, xxl) |
type | enum or string | 分类:1. dom 结构不变、只有皮肤的变化 2.组件类型只有并列的几类 | normal, primary, secondary |
visible | boolean | 是否显示 | |
defaultVisible | boolean | 是否显示(非受控) | |
disabled | boolean | 禁用组件 | |
closable | bool/string | 允许关闭的方式 | |
htmlType | string | 当原生组件与 Fusion 组件的 type 产生冲突时,原生组件使用 htmlType | |
link | string | 链接 | |
dataSource | array | 列表数据源 | [{label, value}, {label, value}] |
has+'属性' | boolean | 拥有某个属性 | 例如 hasArrow, hasHeader, hasClose 等等 |
事件
- 标准事件或者自定义的符合 w3c 标准的事件,命名必须 on 开头, 即 on + 事件名,如 onExpand。
开发指南
- 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
}
- 在项目的入口文件
main.ts
中引入setGlobalApp
、register
方法,用于注册物料到 iking-form的面板中,如下所示:
javascript
import {setGlobalApp, register} from '@/utils/iking-form-extension';
- 将物料注册到物料面板中:
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 */