Skip to content

定义组件

在相应的模块文件夹下每个子文件夹会被解析为一个组件模块,默认导入index.ts 或 index.tsx 作为组件定义文件。

视频教程

TBD

如何新增一个组件

在 widgets 目录下相应的模块文件夹下创建一个文件夹,文件夹名称即为组件名称。在文件夹下创建index.ts文件,配置组件_vid、名称、图标,渲染函数等信息即可,创建 index.render.vue 文件作为组件渲染文件。配置参数参考 组件定义类型

组件定义类型

typescript
// 组件实例
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)。这两层容器分别对应 outerStyleDesignerPropsinnerStyleDesignerProps 配置项。

外容器(Outer Container)

  • 作用:用于包裹整个组件的外层容器,不影响组件内部结构的样式。
  • 实现方式:当组件定义了外容器样式属性时,系统会自动在外层生成一个 <div> 元素,并将 outerStyleDesignerProps 中定义的样式应用到该 <div> 上。

内容器(Inner Container)

  • 作用:主要用于设置组件渲染文件内部的样式。
  • 实现方式:当组件定义了内容器样式属性时系统会将定义的样式通过 :style 绑定到 <component :is="widgetRender" /> 上。
  • 重要限制
    • 如果组件的根元素是多个同级元素(即非单一根节点),或其根元素无法接收 style 属性(例如自定义 Web Component 或某些第三方组件),则直接绑定 style不生效
    • 此时,必须在 index.render.vue 中手动包裹一个 <div>,以确保定义的样式能正确应用。

通过合理使用 outerStyleDesignerPropsinnerStyleDesignerProps,可实现组件在设计器中高度灵活的样式控制,同时保持内部逻辑与外部布局的解耦。

示例

按钮Button组件: 该组件定义了一个基础的按钮配置参数和render文件。

index.ts 定义文件: baseDesignerProps 为基础属性一般为不支持动态绑定的属性,advDesignerProps 为高级属性一般为需要支持动态绑定的属性,但也是不是硬性规定,是否可绑定还是需要通过属性定义时 bindable 参数指定,events 为事件定义为组件内部会触发的事件对应的执行函数。

typescript
// 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 widget

index.render.vue 渲染文件: 该文件定义了组件的渲染逻辑,通过 useWidget hook 获取通用工具函数,可参考useWidget 说明。同时做了相应的扩展,支持尺寸自适应 和 二次确认逻辑。

vue
<!-- 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>