定义组件
在相应的模块文件夹下每个子文件夹会被解析为一个组件模块,默认导入index.ts 或 index.tsx 作为组件定义文件。
视频教程
TBD
如何新增一个组件
在 widgets 目录下相应的模块文件夹下创建一个文件夹,文件夹名称即为组件名称。在文件夹下创建index.ts文件,配置组件_vid、名称、图标,渲染函数等信息即可,创建 index.render.vue 文件作为组件渲染文件。配置参数参考 组件定义类型。
组件定义类型
// 组件实例
export interface WidgetInstance {
/** 组件唯一id */
_vid: string
/** 组件变量名 */
_var?: string
/** 组件key */
_key: string
/** 组件所属模块名称 */
_moduleName: string
/** 在画板中隐藏 */
_hidden?: boolean
/** 组件权限控制 */
_vIfPermis?: string[]
/** v-if 函数控制 */
_vIfFun?: DesignerEditorEvalFunction
/** 组件字段绑定 */
_binds?: Record<string, WidgetPropDefineBind | undefined>
/** 组件中文名称 */
label: string
/** 组件icon */
icon?: string
/** 组件属性 */
props: Record<string, any>
/** 组件属性绑定 */
propsBind?: Record<string, WidgetPropDefineBind | undefined>
/** 所属插槽Key */
slotKey?: string
/** 插槽组件 */
slots: WidgetInstance[]
/** 插槽子组件 */
slotChildren: WidgetInstance[]
/** 数据定义 */
dataDefines?: WidgetDataDefine[]
/** 组件事件绑定 */
eventsBind?: Record<string, DesignerEditorEventBind | undefined>
/** 需求描述 */
prd?: WidgetPrd
}
// 组件定义
export type WidgetDefine = {
/** 组件name */
_key?: string
/** 组件所属模块名称 */
_moduleName?: string
/** 组件icon */
icon?: string
/** 组件Tips */
tips?: string
/** 组件排序 */
sort?: number
/** 组件属性 */
props?: Record<string, any>
/** 组件设计器基础属性 */
baseDesignerProps?: WidgetPropDefine[]
/** 组件设计器高级属性 */
advDesignerProps?: WidgetPropDefine[]
/** 组件设计器样式属性 */
styleDesignerProps?: WidgetPropDefine[]
/** 禁用组件外容器 */
disableOuter?: boolean
/** 组件外容器样式属性 */
outerStyleDesignerProps?: WidgetPropDefine[]
/** 禁用组件内容器 */
disableInner?: boolean
/** 组件内容器样式属性 */
innerStyleDesignerProps?: WidgetPropDefine[]
/** 组件事件定义 */
events?: DesignerEditorEventDefine[]
/** 组件创建实例钩子方法 */
create?: (
editor: DesignerEditor,
define: WidgetDefine,
args?: {
parentWidget?: WidgetInstance
parentRenderContext?: WidgetRenderContext
options?: WidgetItemOptions
defaultProps?: Record<string, any>
}
) => WidgetInstance | Promise<WidgetInstance>
/** 组件渲染函数 */
render: (args: WidgetRenderProps) => () => JSX.Element
/** 更新组件属性 */
saveProps?: (
editor: DesignerEditor,
widget: WidgetInstance,
propKey: string,
propValue: any
) => void
/** 更新组件属性绑定 */
savePropsBind?: (
editor: DesignerEditor,
widget: WidgetInstance,
propKey: string,
propsBindValue: any
) => void
/** 更新组件事件绑定 */
saveEventBind?: (
editor: DesignerEditor,
widget: WidgetInstance,
eventKey: string,
eventBindValue?: DesignerEditorEventBind
) => void
/** 禁止定义数据 */
enableDataDefineTypes?: ('def' | 'remote' | 'ref')[]
/** 组件菜单 */
menus?: (editor: DesignerEditor, args: UseWidgetMenusArgs) => MenuItem[]
/** 是否菜单中禁用 */
disableInMenu?: (
action: WidgetMenuAction,
widget?: WidgetInstance,
widgetRenderContext?: WidgetRenderContext
) => boolean | ComputedRef<boolean>
/** 是否菜单中隐藏 */
hiddenInMenu?: (
action: WidgetMenuAction,
widget?: WidgetInstance,
widgetRenderContext?: WidgetRenderContext
) => boolean | ComputedRef<boolean>
/** 运行时数据定义 */
runtimeDataDefines?: (editor: DesignerEditor, widget: WidgetInstance) => WidgetDataDefine[]
} & Omit<WidgetInstance, '_key' | '_moduleName' | 'props' | 'slots' | 'slotChildren'>外容器与内容器
为了支持对组件在设计器中的布局、定位、边距、背景等通用样式的灵活控制,系统为每个组件提供了两层容器结构:外容器(Outer Container) 和 内容器(Inner Container)。这两层容器分别对应 outerStyleDesignerProps 与 innerStyleDesignerProps 配置项。
外容器(Outer Container)
- 作用:用于包裹整个组件的外层容器,不影响组件内部结构的样式。
- 实现方式:当组件定义了外容器样式属性时,系统会自动在外层生成一个
<div>元素,并将outerStyleDesignerProps中定义的样式应用到该<div>上。
内容器(Inner Container)
- 作用:主要用于设置组件渲染文件内部的样式。
- 实现方式:当组件定义了内容器样式属性时系统会将定义的样式通过
:style绑定到<component :is="widgetRender" />上。 - 重要限制:
- 如果组件的根元素是多个同级元素(即非单一根节点),或其根元素无法接收
style属性(例如自定义 Web Component 或某些第三方组件),则直接绑定style将不生效。 - 此时,必须在
index.render.vue中手动包裹一个<div>,以确保定义的样式能正确应用。
- 如果组件的根元素是多个同级元素(即非单一根节点),或其根元素无法接收
通过合理使用 outerStyleDesignerProps 和 innerStyleDesignerProps,可实现组件在设计器中高度灵活的样式控制,同时保持内部逻辑与外部布局的解耦。
示例
按钮Button组件: 该组件定义了一个基础的按钮配置参数和render文件。
index.ts 定义文件: baseDesignerProps 为基础属性一般为不支持动态绑定的属性,advDesignerProps 为高级属性一般为需要支持动态绑定的属性,但也是不是硬性规定,是否可绑定还是需要通过属性定义时 bindable 参数指定,events 为事件定义为组件内部会触发的事件对应的执行函数。
// index.tsx
import Render from './index.render.vue'
import {
ElCommonSizeOptions,
ElCommonTypeOptions,
WidgetDefine
} from '../../../designer-editor.type'
import {
eventDefine,
inputDefine,
selectDefine,
switchDefine
} from '../../../designer-editor.props'
const widget: WidgetDefine = {
label: '按钮Button',
icon: 'svg-icon:lowcode-icon-button',
render: (args) => () => {
return <Render {...args} />
},
baseDesignerProps: [
selectDefine({ key: 'size', label: '按钮尺寸' }, ElCommonSizeOptions),
selectDefine({ key: 'type', label: '按钮类型' }, ElCommonTypeOptions),
switchDefine({ key: 'plain', label: '是否为朴素按钮' }),
switchDefine({ key: 'text', label: '是否为文字按钮' }),
switchDefine({ key: 'link', label: '是否为链接按钮' }),
switchDefine({ key: 'round', label: '是否为圆角按钮' }),
switchDefine({ key: 'circle', label: '是否为圆形按钮' }),
switchDefine({ key: 'loading', label: '是否加载中状态' }),
switchDefine({ key: 'disabled', label: '是否禁用状态' })
],
advDesignerProps: [
switchDefine({ key: 'auto', label: '是否尺寸自适应' }),
inputDefine({ key: 'label', label: '按钮文字', defaultValue: '按钮' }),
switchDefine({ key: 'showConfirm', label: '是否二次确认' }),
inputDefine({
key: 'confirmMsg',
label: '二次确认提示',
isShow: ({ widget }) => widget.props.showConfirm
})
],
events: [
eventDefine('click', { type: 'mouse-function', label: '按钮点击' })
]
}
export default widgetindex.render.vue 渲染文件: 该文件定义了组件的渲染逻辑,通过 useWidget hook 获取通用工具函数,可参考useWidget 说明。同时做了相应的扩展,支持尺寸自适应 和 二次确认逻辑。
<!-- index.render.vue -->
<template>
<el-button v-bind="buttonAttrs" @click="onClick">
{{ label }}
</el-button>
</template>
<script lang="ts" setup>
import { isEmpty } from '@/utils/is'
import { useWidget, type WidgetRenderProps } from '../../hooks'
const props = defineProps<WidgetRenderProps>()
const { usePropValue, usePropAndEvent, useEventBind, toEvalFunction } = useWidget(props)
const label = computed(() => usePropValue('label'))
const message = useMessage()
const buttonAttrs = computed(() => {
return {
...usePropAndEvent({ omit: ['label', 'click', 'showConfirm', 'confirmMsg'] }),
style: usePropValue('auto') ? { width: '100%', height: '100%' } : undefined
}
})
const onClickHandler = computed(() => toEvalFunction(useEventBind('click')))
const onClick = async (e) => {
if (usePropValue('showConfirm')) {
const confirmMsg = usePropValue('confirmMsg')
await message.confirm(isEmpty(confirmMsg) ? '是否确认执行' : confirmMsg)
}
await onClickHandler.value?.(e)
}
</script>