添加新组件
自定义组件可以被集成到设计器中,用于扩展表单渲染能力与设计态配置能力。本文会用项目内置的 SignaturePad(手写签名) 作为参考样例,讲清楚“新增一个组件”在项目里要补齐哪些环节,并给出一个从 0 到 1 的完整扩展示例。
在渲染器文档中可以查看内置表单组件及其所有可配置参数。
在自定义表单组件可查阅自定义表单组件的开发规范与标准要求。
在源码中可以参考现有组件的拖拽规则实现(如 src/config/rule/*)。
注意
设计器已支持通过 addDragRule 全局注册拖拽规则,实现一次配置多处使用。
新增组件要改哪些地方?
在设计器里新增一个“可拖拽 + 可配置 + 可渲染”的组件,通常涉及 4 层:
- 表单渲染组件(Runtime Component):真正参与 v-model、触发事件的 Vue 组件(PC/移动端通常各一份)
- 渲染器注册(form-create 注册):把组件注册进渲染器,使
rule.type能渲染出对应组件 - 拖拽规则(DragRule):决定左侧面板展示、拖入后生成的 rule、右侧属性面板、事件、多语言等
- 设计器画布注册(designerForm 注册):设计态画布也是一个表单渲染环境,需要能渲染你的组件
参考样例:SignaturePad 的接入链路
SignaturePad 是一个典型“表单组件 + 只读预览特殊展示”的例子。相关文件一般可以在以下位置找到:
- 组件实现(PC):
src/components/SignaturePad.vue - 组件实现(Mobile):
src/components/mobile/SignaturePad.vue - 拖拽规则:
src/config/rule/signaturePad.js - 渲染器注册(PC/移动):
src/form/elm.js、src/form/mobile.js - 设计器入口注册:
src/index.js
1. 命名建议(避免“找不到组件”)
你会看到 SignaturePad 存在“大小写不一致”的情况:
- DragRule
name/rule.type:signaturePad - Vue 组件
name/ 注册 key:SignaturePad
为了避免踩坑,新增组件时建议直接做到:
rule.type与注册 key 完全一致(最稳)- 如果历史原因必须兼容两套命名,可使用双注册兜底:
formCreate.component('myType', MyComp);
formCreate.component('MyComp', MyComp);2. 组件实现要点(建议遵守)
以 SignaturePad 的实现风格为参照,推荐你的组件至少具备:
- v-model 规范:
modelValue+update:modelValue事件 (Vue2中是value+input事件) - 事件透出:必要时同时触发
change(便于统一监听) - 禁用态:支持
disabled - 注入对象:如
formCreateInject(用于多语言、options、api 等)
3. 阅读模式适配(按需)
像签名、上传、富文本这类组件,在“阅读模式/预览模式”下往往需要特殊渲染(例如只展示图片)。SignaturePad 就在预览逻辑里对 type === 'signaturePad' 做了分支处理:
- PC:一般在
src/utils/preview.js - Mobile:一般在
src/form/mobile.js的defaultPreview - 或者在组件中使用自定义渲染方式
扩展示例:新增一个 UserSelect 用户选择组件
下面给出一个可直接照抄的示例:新增一个 UserSelect 组件,可选择用户并输出 string(用户 id)。你可以把它替换为你自己的业务组件。
1. 新增组件文件
1.1 PC 组件(ElementPlus)
新建 src/components/UserSelect.vue:
<template>
<div class="_fc-user-select">
<!-- 阅读模式下:自定义回显 -->
<template v-if="formCreateInject?.preview">
<span class="_fc-read-view">{{ previewText }}</span>
</template>
<!-- 编辑模式下:正常表单交互 -->
<template v-else>
<el-select
v-bind="$attrs"
:modelValue="modelValue"
:disabled="disabled"
:placeholder="placeholder"
@update:modelValue="onChange"
style="width: 100%;"
>
<el-option v-for="u in options" :key="u.value" :label="u.label" :value="u.value" />
</el-select>
</template>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'UserSelect',
emits: ['update:modelValue', 'change'],
props: {
modelValue: [String, Number],
disabled: Boolean,
placeholder: String,
// 注入 formCreate 上下文(用于判断阅读模式 preview)
formCreateInject: Object,
// 简化示例:直接通过 props 传 options;真实业务也可以通过 formCreateInject / request 动态拉取
options: {
type: Array,
default: () => [],
},
},
computed: {
previewText() {
const val = this.modelValue;
const hit = (this.options || []).find(o => o?.value === val);
return hit?.label ?? (val == null || val === '' ? '' : String(val));
},
},
methods: {
onChange(val) {
this.$emit('update:modelValue', val);
this.$emit('change', val);
},
},
});
</script>1.2 Mobile 组件(Vant,可选)
如果你的项目支持移动端渲染,建议同步提供一个移动端版本。新建 src/components/mobile/UserSelect.vue(示例用 van-field + van-picker 简化):
<template>
<div class="_fc-m-user-select">
<!-- 阅读模式下:自定义回显 -->
<template v-if="formCreateInject?.preview">
<span class="_fc-read-view">{{ label }}</span>
</template>
<!-- 编辑模式下:正常表单交互 -->
<template v-else>
<van-field
:modelValue="label"
:disabled="disabled"
:placeholder="placeholder"
readonly
is-link
@click="open"
/>
<van-popup v-model:show="show" position="bottom">
<van-picker
:columns="options"
@confirm="confirm"
@cancel="show = false"
/>
</van-popup>
</template>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'UserSelect',
emits: ['update:modelValue', 'change'],
props: {
modelValue: [String, Number],
disabled: Boolean,
placeholder: String,
// 注入 formCreate 上下文(用于判断阅读模式 preview)
formCreateInject: Object,
// Vant Picker columns: [{ text, value }]
options: { type: Array, default: () => [] },
},
data() {
return { show: false };
},
computed: {
label() {
const hit = (this.options || []).find(o => o.value === this.modelValue);
return hit ? hit.text : '';
},
},
methods: {
open() {
if (!this.disabled) this.show = true;
},
confirm(item) {
const val = item?.value;
this.$emit('update:modelValue', val);
this.$emit('change', val);
this.show = false;
},
},
});
</script>2. 注册组件(渲染器 + 设计器画布)
PC/移动渲染器要能渲染,设计器画布也要能渲染。
2.1 PC 渲染器注册
在 src/form/elm.js 中引入并注册:
import UserSelect from '../components/UserSelect.vue';
formCreate.component('UserSelect', UserSelect);2.2 Mobile 渲染器注册(如果支持)
在 src/form/mobile.js 中引入并注册:
import UserSelect from '../components/mobile/UserSelect.vue';
formCreateMobile.component('UserSelect', UserSelect);2.3 设计器入口注册
在 src/index.js 中引入并注册:
import UserSelect from './components/UserSelect.vue';
addComponent('UserSelect', UserSelect);如果你的项目把“设计器画布注册”和“渲染器注册”拆开了,请确保两边都注册到了,否则会出现“能拖拽但画布不显示”的问题。
3. 定义拖拽规则(右侧属性面板)
新建 src/config/rule/userSelect.js:
import uniqueId from '@form-create/utils/lib/unique';
import { localeProps } from '../../utils';
const label = '用户选择';
const name = 'UserSelect';
export default {
menu: 'main',
icon: 'icon-select',
label,
name,
input: true,
mask: true,
validate: ['string', 'number'],
event: ['change'],
rule({ t }) {
return {
type: name,
field: uniqueId(),
// 关键:开启自定义阅读模式(组件内部通过 formCreateInject.preview 回显)
readMode: 'custom',
title: t('com.userSelect.name') || label,
info: '',
$required: false,
props: {
placeholder: t('com.userSelect.props.placeholder') || '请选择用户',
// 示例:默认静态 options;你也可以只提供空数组,运行时由业务层注入
options: [
{ label: '张三', value: '1' },
{ label: '李四', value: '2' },
],
},
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
{ type: 'switch', field: 'disabled' },
{ type: 'input', field: 'placeholder' },
// 这里给出一种常见方式:用 TableOptions/TreeOptions 等配置器管理 options
// 不同项目的 options 配置器名称可能不同,请按现有组件规则对齐
]);
},
};重要提示
readMode: 'custom' 只对后续新拖入设计器的规则生效。对于已经保存的规则不会自动补上该配置;若要对已有规则启用自定义阅读模式,需要手动编辑规则或重新拖拽组件。
4. 导入拖拽规则(让左侧面板出现)
在 src/config/index.js 中:
import userSelect from './rule/userSelect';
// 放入 ruleList 合适的位置
ruleList.push(userSelect);5. 补齐多语言(建议)
在 src/locale/zh-cn.js 的 com 节点中新增:
userSelect: {
name: '用户选择',
props: {
disabled: '禁用',
placeholder: '占位文字',
},
},并建议同步维护 en.js、jp.js(避免切换语言后配置项缺失)。
6. 阅读模式显示(示例)
阅读模式的开启与自定义回显方式,参考 src/preview.md:
- 开启阅读模式:在表单
options中设置options.preview = true - 自定义回显:组件内部通过
formCreateInject.preview分支渲染回显内容(前提是 DragRule 已设置readMode: 'custom')
对 UserSelect 来说:
- 编辑模式:显示下拉框 / Picker
- 阅读模式:显示当前值对应的用户名称(找不到则回退显示原始值)
布局组件开发参考
布局组件(如栅格布局 Row、Col)主要用于页面布局,不收集表单数据。参考实现:
- Row 组件:
src/config/rule/row.js - Col 组件:
src/config/rule/col.js
布局组件特点
- 不收集数据:通常设置
input: false或不设置input属性 - 支持嵌套子组件:通过
children属性管理子规则
Row 组件示例
import { localeProps } from '../../utils';
const label = '栅格布局';
const name = 'fcRow';
export default {
menu: 'layout', // 左侧面板分类
icon: 'icon-row',
label,
name,
mask: false, // 布局组件通常不需要遮罩
children: 'col', // 指定子组件类型
childrenLen: 2, // 默认子组件数量
rule() {
return {
type: name,
props: {},
children: [], // 布局组件必须有 children
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
{
type: 'inputNumber',
field: 'gutter', // 栅格间距
props: { min: 0 },
},
{
type: 'switch',
field: 'type',
props: { activeValue: 'flex', inactiveValue: 'default' },
},
{
type: 'select',
field: 'justify', // flex 布局下的水平排列方式
options: [
{ label: 'start', value: 'start' },
{ label: 'end', value: 'end' },
{ label: 'center', value: 'center' },
{ label: 'space-around', value: 'space-around' },
{ label: 'space-between', value: 'space-between' },
],
},
{
type: 'select',
field: 'align', // flex 布局下的垂直对齐方式
options: [
{ label: 'top', value: 'top' },
{ label: 'middle', value: 'middle' },
{ label: 'bottom', value: 'bottom' },
],
},
]);
},
};Col 组件示例
import { localeProps } from '../../utils';
const name = 'col';
// 响应式断点配置
const devices = {
xs: '<768px',
sm: '≥768px',
md: '≥992px',
lg: '≥1200px',
xl: '≥1920px',
};
export default {
name,
label: '格子',
drag: true,
dragBtn: false,
inside: true, // 只能拖入到特定父组件内
mask: false,
rule() {
return {
type: name,
props: { span: 12 }, // 默认占 12 列(24 栅格系统)
children: [],
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
// 基础配置
{
type: 'slider',
field: 'span',
value: 12,
props: { min: 0, max: 24 }
},
{
type: 'slider',
field: 'offset', // 栅格左侧的间隔格数
props: { min: 0, max: 24 }
},
{
type: 'slider',
field: 'push', // 栅格向右移动格数
props: { min: 0, max: 24 }
},
{
type: 'slider',
field: 'pull', // 栅格向左移动格数
props: { min: 0, max: 24 }
},
// 响应式配置(使用 ConfigItem + elTabs 组织)
{
type: 'ConfigItem',
props: {
label: t('props.reactive'),
warning: t('com.col.info'),
},
children: [
{
type: 'elTabs',
style: { width: '100%' },
slot: 'append',
children: Object.keys(devices).map(k => {
return {
type: 'elTabPane',
props: { label: devices[k] },
style: 'padding:0 10px;',
children: [
{
type: 'slider',
field: k + '>span', // 使用 > 分隔符表示嵌套属性
title: t('com.col.props.span'),
value: 12,
props: { min: 0, max: 24 },
},
{
type: 'slider',
field: k + '>offset',
title: t('com.col.props.offset'),
props: { min: 0, max: 24 },
},
// ... 其他响应式属性
],
};
}),
},
],
},
]);
},
};布局组件开发要点
inside: true:限制组件只能拖入到特定父组件内(如 Col 只能拖入 Row)children属性:指定允许的子组件类型- 响应式配置:使用
field: 'xs>span'这种格式表示嵌套属性,配合ConfigItem+elTabs组织复杂配置
子表单组件开发参考
子表单组件用于收集嵌套的表单数据,支持对象(object)和数组(array)两种数据结构。参考实现:
- 分组子表单(subForm):
src/config/rule/subForm.js- 收集对象数据 - 表格表单(tableForm):
src/config/rule/tableForm.js- 收集数组数据 - 表格表单列(tableFormColumn):
src/config/rule/tableFormColumn.js- 表格表单的列配置
子表单组件特点
- 数据收集:设置
input: true和subForm: 'object'或subForm: 'array' - 数据转换:通过
loadRule和parseRule方法处理设计态和运行态的数据格式转换 - 嵌套规则:子表单内部可以包含其他表单组件
分组子表单(subForm)示例
import { localeProps } from '../../utils';
import uniqueId from '@form-create/utils/lib/unique';
const label = '分组';
const name = 'subForm';
export default {
menu: 'subform',
icon: 'icon-group',
label,
name,
input: true, // 收集数据
inside: false,
drag: true,
dragBtn: true,
mask: false,
subForm: 'object', // 收集对象类型数据
event: ['change'],
// 加载规则:将运行态数据转换为设计态格式
loadRule(rule) {
// 运行态:rule.props.rule 存储子规则数组
// 设计态:rule.children 存储子规则数组
rule.children = rule.props.rule || [];
rule.type = 'FcRow'; // 设计态使用 FcRow 渲染
delete rule.props.rule;
},
// 解析规则:将设计态数据转换为运行态格式
parseRule(rule) {
// 设计态 → 运行态
rule.props.rule = rule.children;
rule.type = 'subForm';
delete rule.children;
},
rule({ t }) {
return {
type: 'fcRow', // 设计态类型
field: uniqueId(),
title: t('com.subForm.name'),
info: '',
$required: false,
props: {},
children: [], // 子规则数组
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
{
type: 'switch',
field: 'disabled',
},
]);
},
};表格表单(tableForm)示例
import unique from '@form-create/utils/lib/unique';
import { localeProps } from '../../utils';
const label = '表格表单';
const name = 'tableForm';
export default {
menu: 'subform',
icon: 'icon-table-form',
label,
name,
input: true,
mask: false,
subForm: 'array', // 收集数组类型数据
languageKey: ['add', 'operation', 'dataEmpty'], // 需要多语言支持的 key
event: ['change', 'add', 'delete'],
children: 'tableFormColumn', // 子组件类型(列配置)
// 加载规则:运行态 → 设计态
loadRule(rule) {
if (!rule.props) rule.props = {};
const columns = rule.props.columns || [];
// 将 columns 配置转换为 tableFormColumn 子规则
rule.children = columns.map(column => {
return {
type: 'tableFormColumn',
_fc_drag_tag: 'tableFormColumn',
props: {
label: column.label,
align: column.align,
required: column.required || false,
width: column.style.width || '',
color: column.style.color || '',
},
children: column.rule || [], // 每列的表单规则
};
});
delete rule.props.columns;
},
// 解析规则:设计态 → 运行态
parseRule(rule) {
const children = rule.children || [];
// 将 tableFormColumn 子规则转换为 columns 配置
rule.props.columns = children.map(column => {
return {
label: column.props.label,
required: column.props.required,
align: column.props.align,
style: {
width: column.props.width,
color: column.props.color,
},
rule: column.children || [], // 每列的表单规则
};
});
rule.children = [];
},
rule({ t }) {
return {
type: name,
field: unique(),
title: t('com.tableForm.name'),
info: '',
props: {},
children: [],
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
{
type: 'switch',
field: 'disabled',
},
{
type: 'switch',
field: 'addable', // 是否可添加行
value: true,
},
{
type: 'switch',
field: 'deletable', // 是否可删除行
value: true,
},
{
type: 'switch',
field: 'filterEmptyColumn', // 是否过滤空列
value: true,
},
{
type: 'inputNumber',
field: 'min', // 最小行数
props: { min: 0 },
},
{
type: 'inputNumber',
field: 'max', // 最大行数
props: { min: 0 },
},
]);
},
};表格表单列(tableFormColumn)示例
import { localeProps, localeOptions } from '../../utils';
const name = 'tableFormColumn';
export default {
icon: 'icon-cell',
name,
aide: true, // 辅助组件(不在左侧面板显示)
drag: true,
dragBtn: false,
mask: false,
style: false, // 不显示样式配置
advanced: false, // 不显示高级配置
variable: false, // 不显示变量配置
rule({ t }) {
return {
type: name,
props: {
label: t('com.tableFormColumn.label'),
width: 'auto',
},
children: [], // 该列包含的表单规则
};
},
props(_, { t }) {
return localeProps(t, name + '.props', [
{
type: 'input',
field: 'label', // 列标题
},
{
type: 'select',
field: 'align', // 对齐方式
options: localeOptions(t, [
{ label: 'left', value: 'left' },
{ label: 'center', value: 'center' },
{ label: 'right', value: 'right' },
]),
},
{
type: 'switch',
field: 'required', // 是否必填
},
{
type: 'input',
field: 'width', // 列宽
},
{
type: 'ColorInput',
field: 'color', // 列颜色
},
]);
},
};子表单组件开发要点
subForm属性:'object':收集对象数据,如{ name: 'xxx', age: 18 }'array':收集数组数据,如[{ name: 'xxx' }, { name: 'yyy' }]
loadRule方法:- 在加载规则时调用(运行态 → 设计态)
- 将运行态的数据格式转换为设计态可编辑的格式
- 例如:将
props.columns转换为children数组
parseRule方法:- 在保存规则时调用(设计态 → 运行态)
- 将设计态的格式转换为运行态的数据格式
- 例如:将
children数组转换为props.columns
嵌套字段路径:
- 子表单内的组件字段路径会自动拼接父字段
- 例如:父字段
user,子字段name,最终路径为user.name
children属性:- 指定子组件类型(如
children: 'tableFormColumn')
- 指定子组件类型(如
辅助组件标记:
- 设置
aide: true表示该组件不在左侧面板显示 - 只能通过父组件的
children属性关联使用
- 设置
常见问题排查
1. 能拖拽但画布不显示
- 设计器画布未注册组件(缺
designerForm.component或入口addComponent) rule.type与注册 key 不一致
2. 运行态可渲染但设计态不行(或反过来)
- 只注册了其中一侧(运行态 formCreate / 设计态 designerForm)
- PC/移动端注册不一致导致跨端预览异常
3. 右侧配置改了没效果
- 组件未使用对应的 props
- 拖拽规则
props()的field没映射到你真正读取的字段(通常是rule.props.xxx)


