Skip to content

表单相关组件封装方案

与对外文档的关系

一、命名规范

类型规范示例
组件(不与 EP 冲突)el-xxx<el-form-layout>
组件(与 EP 冲突)对外导出加 Lab 等区分;目录 config-providerElLabConfigProvider
TypeScript 类型EllXxxEllFormSchema
ComposableuseXxxuseGridLayoutuseConfigProvider
InjectionKey语义化 + _INJECTION_KEYCONFIG_PROVIDER_INJECTION_KEY

二、整体架构

2.1 组件层级

config-provider(ElConfigProvider)   ← 全局配置(网格等,可扩展)

├── el-form-layout                         ← 纯 Grid 布局层(供 builder 等使用)
│   └── el-form-layout-item                ← 布局单元(封装 el-form-item)

├── el-form-renderer                       ← 控件渲染引擎(插件注册;编辑 / 禁用 / 预览)

├── el-form-query                          ← 查询表单:自管 el-form + Grid + 控件区
│   ├── 不内嵌 el-form-layout(见下说明)
│   ├── 独立布局实现 + dense 折叠算法(操作区占折叠后末行末列)
│   └── el-form-renderer 渲染表单项控件

└── el-form-builder                        ← 完整表单(新建/编辑)
    ├── el-form + el-form-layout 提供布局
    ├── el-form-group 提供分组(复用已有组件)
    └── el-form-renderer 渲染控件

职责对照(最初设计)

组件职责
el-form-layout / el-form-layout-item基于 Grid 的布局;el-form-layout-item 内封装 el-form-item
el-form-renderer按 schema 渲染控件;插件式注册;支持展示、禁用、预览等模式。
el-form-query封装 el-form自带 Grid 布局(不组合 el-form-layout),表单项由 el-form-renderer 渲染。
el-form-builder封装 el-form,布局交给 el-form-layout,控件交给 el-form-renderer
config-provider / ElLabConfigProvider注册默认 grid 等全局配置(可扩展服务其它组件)。
useFormDialog / useFormDrawer在现有 useDialog / useDrawer 上接入 el-form-builder
el-cru-page组合 el-simple-page-headerel-form-builder

为何 el-form-query 不集成 el-form-layout

查询表单在折叠时需要让操作按钮(查询 / 重置等)落在当前折叠区域的最后一行、最后一列,与纯「字段网格」的布局目标不同,必须配合 dense 布局算法(如折叠态下 simulateDenseFill 等)单独演进。因此 el-form-query 不在组件树中组合 el-form-layout,仅在 utils 层复用断点、列数解析、useGridLayout工具能力(或与专用 useFormQueryLayout 共享底层);dense 与操作区落位逻辑只存在于查询表单一侧。

2.2 共享层目录

packages/
├── utils/src/
│   ├── breakpoints.ts            ← 断点常量 + 工具函数
│   ├── form-builder-control-registry.ts  ← FORM_BUILDER_FIELD_REGISTRY_KEY、EllFormBuilderFieldRegistry
│   ├── form-builder-field-path.ts        ← EllFormBuilderFieldRefSpec、路径归一化(点分 / address[2])
│   ├── form-schema.ts            ← EllFormSchema 类型 + parseSchema
│   ├── grid-layout.ts            ← 预设解析、`resolveColCountFromGridLayout`
│   ├── use-grid-layout.ts        ← 列数 + `grid-template-columns`(`ResizeObserver`;可选 **`fixedColCount`**)
│   ├── simulate-dense-fill.ts    ← `simulateDenseFillCollapsed` / `simulateDenseFillExpanded`、折叠·展开装箱
│   └── form-label-overflow.ts    ← 标签溢出测量(FormQuery / FormLayoutItem 共用)

└── components/

    │   ── 基础表单组件 ──
    ├── config-provider/
    │   ├── __tests__/
    │   ├── src/
    │   │   └── types.ts
    │   └── index.ts               ← config-provider(ElLabConfigProvider)

    ├── form-layout/
    │   ├── __tests__/
    │   ├── src/
    │   │   ├── form-layout.vue
    │   │   ├── form-layout-item.vue
    │   │   └── types.ts
    │   └── index.ts               ← el-form-layout + el-form-layout-item

    ├── form-renderer/
    │   ├── __tests__/
    │   ├── src/
    │   │   ├── form-renderer.vue
    │   │   ├── controls/          ← 内置预览组件
    │   │   │   ├── text-preview.vue
    │   │   │   ├── select-preview.vue
    │   │   │   └── date-preview.vue
    │   │   └── types.ts
    │   └── index.ts               ← el-form-renderer

    ├── form-query/
    │   ├── __tests__/
    │   ├── src/
    │   │   ├── form-query.vue
    │   │   └── types.ts
    │   └── index.ts               ← el-form-query

    ├── form-builder/
    │   ├── __tests__/
    │   ├── src/
    │   │   ├── form-builder.vue
    │   │   └── types.ts
    │   └── index.ts               ← el-form-builder(分组渲染使用已有的 el-form-group)

    │   ── 组合式 ──
    ├── hooks/
    │   └── use-form/
    │       ├── __tests__/
    │       ├── src/
    │       │   ├── types.ts
    │       │   └── use-form.ts    ← useForm()
    │       └── index.ts

    │   ── 业务复合组件 ──
    ├── form-dialog/
    │   ├── __tests__/
    │   ├── src/
    │   │   └── types.ts
    │   └── index.ts               ← useFormDialog()

    ├── form-drawer/
    │   ├── __tests__/
    │   ├── src/
    │   │   └── types.ts
    │   └── index.ts               ← useFormDrawer()

    └── cru-page/
        ├── __tests__/
        ├── src/
        │   ├── cru-page.vue
        │   └── types.ts
        └── index.ts               ← el-cru-page

文档目录(示例与演示):

docs/content/components/
├── ...                            ← 已有组件文档
└── business/                      ← 业务组件文档
    ├── form-dialog.md
    ├── form-drawer.md
    └── cru-page.md

三、Schema 类型设计

3.1 类型标记

通过显式的 type 字段区分 schema 项,避免用 children 存在与否做隐式判断:

ts
type EllSchemaItemType = 'field' | 'group' | 'custom'

/**
 * 字段级标签配置:**`el-form-builder` / `el-form-layout-item`** 与 **`label`、顶层 `labelWidth`** 配合使用。
 * 除 **`EllFormLayoutItemLabelConfig`** 已有项外,纳入查询侧常用的 **`description` / `renderLabel`**。
 */
interface EllFormFieldLabelConfig extends EllFormLayoutItemLabelConfig {
  /**
   * 字段说明;有值时标签旁默认 **`QuestionFilled` + Tooltip**(与 **`el-form-query`** 一致)。
   * **`renderLabel`** 存在时不自动展示该图标。
   */
  description?: string
  /** 自定义标签渲染(覆盖 **`description`** 的默认图标逻辑) */
  renderLabel?: (label: string) => VNodeChild
}

// 字段项(type 默认为 'field',可省略)
interface EllFieldSchema {
  type?: 'field'
  field: string
  /** 表单项标签文案(顶层快捷,与 **`ElFormLayoutItem`** 一致) */
  label?: string
  /**
   * 当前字段标签列宽(顶层快捷,映射为内层 **`labelConfig.labelWidth`** 的缺省值)。
   * 与 **`labelConfig.labelWidth`** 同时存在时 **以 `labelConfig` 为准**。
   */
  labelWidth?: number | string
  /**
   * 除 **`label`**、**`labelWidth`** 外**所有标签相关**(位置、**`justify`**、溢出、`autoLabelWidth`、**`description`**、**`renderLabel`** 等)。
   * 若与 **`formItemConfig.formItemConfig`**(内层透传包)或 custom 块 **`layoutFormItem`** 中的标签相关键同键冲突:**本字段的 `labelConfig`** 优先(实现时与 **`ElFormLayoutItem`** 合并规则对齐)。
   */
  labelConfig?: EllFormFieldLabelConfig
  controlType?: EllControlType | string

  // ── 控件配置 ──────────────────────────────────────────────
  /** 透传给控件的 props(内层同名属性覆盖外层) */
  controlConfig?: Record<string, any>
  /** 控件插槽(如 el-input 的 prefix/suffix) */
  slots?: Record<string, () => VNodeChild>
  /** 控件事件 */
  events?: Record<string, Function>

  // ── 选项 ──────────────────────────────────────────────────
  /**
   * 选项数据(静态数组或异步函数)。
   * 异步函数接收 { model },可在函数内部读取关联字段值。
   * 选项函数重算依赖 **`schema.options`** 解析链中对 **`model`** 的订阅;需细化联动键时在字段上使用 **`dependencies.triggerFields`**(见声明式联动)。
   */
  options?: EllOption[] | EllOptionsResolver

  // ── 校验 ──────────────────────────────────────────────────
  rules?: FormItemRule | FormItemRule[]
  required?: boolean

  // ── 联动 ──────────────────────────────────────────────────
  /**
   * 函数式显隐(**已实现**):与 **`dependencies.show`** 为 **且** 关系;返回 **`false`** 时不挂载表单项。
   * **`payload.tab`** 来自 **`ElFormBuilder` / `ElFormQuery`** 的 **`activeTab`**;**`index`** 为当前层列表下标(根级 **`schema` / `fields` 块 / `group.children` 各算各的)。
   * **`payload.schema`** 为当前字段项数据形态(**`EllFieldSchemaBase`**),不含本回调自身。
   */
  visible?: (payload: EllFieldVisiblePayload) => boolean

  /** 函数式联动:控制禁用状态(**未实现**,请使用 **`dependencies.disabled`**) */
  disabled?: (model: Record<string, any>) => boolean

  /**
   * 声明式联动(**已实现**,与仓库 **`EllFieldSchemaDependencies`** / **`evaluateFieldDependencies`** 对齐)。
   * - **`show`**:`false` 时不挂载表单项(**不**参与校验)。
   * - **`rules` / `required`**:与 **`formItemConfig`** 静态配置合并,动态返回值优先。
   * - **`disabled` / `controlConfig`**:在 **`ElFormRenderer`** 编辑态合并;**`controlConfig`** 动态键覆盖静态同名键。
   * - **`triggerFields`**:可选;不传时依赖 **`model` 所有顶层键** 触发重算。
   */
  dependencies?: {
    triggerFields?: string[]
    show?: (values: Record<string, unknown>) => boolean
    disabled?: (values: Record<string, unknown>) => boolean
    required?: (values: Record<string, unknown>) => boolean
    rules?: (values: Record<string, unknown>) => unknown
    controlConfig?: (values: Record<string, unknown>) => Record<string, unknown>
  }

  // ── 值处理 ──────────────────────────────────────────────────
  default?: any
  modifiers?: EllModelModifier[]
  /** 提交时对值做转换,不修改原始 model */
  transform?: (value: any, model: Record<string, any>) => any
  /** 日期区间字段映射,如 ['startDate', 'endDate'] */
  rangeField?: [string, string]

  // ── 展示 ──────────────────────────────────────────────────
  /**
   * 自定义带选项控件(如 pd-select)的选项渲染。
   * 优先级高于 options 属性。
   */
  renderOption?: (option: EllOption, schema: EllFormSchema) => VNodeChild
  /** 自定义整个字段渲染(最高优先级,覆盖 controlType) */
  render?: (payload: EllRenderPayload) => VNodeChild

  // ── 样式 ──────────────────────────────────────────────────
  /** CSS 类名 */
  class?: string
  /** CSS 样式 */
  style?: string | Record<string, any>

  // ── 布局(由 el-form-layout-item 使用)─────────────────────
  /**
   * 网格中占据列数(列合并),与 `ElFormLayoutItemProps.span` 同语义。
   * - `el-form-query`:参与 `simulateDenseFillCollapsed` / `simulateDenseFillExpanded`。
   * - `el-form-layout`:可作为 `el-form-layout-item` 的便捷 `span` 入口(亦可写在 `formItemConfig.span`)。
   */
  span?: number
  /**
   * 布局与 **`el-form-item`** 透传(**不含** **`label` / `labelWidth` / `labelConfig`**,请用顶层 **`label*`**)。
   * 典型:**`span`**、**`gridColumn`**、**`prop`**(可与顶层 **`field`** 二选一由实现定夺)、**`rules`**、**`message`**、**`formItemConfig`**、**`class`**、**`style`**。
   * 同名属性内层(**`formItemConfig`**)覆盖外层(**`el-form-layout`** / **builder** 全局)。
   */
  formItemConfig?: Omit<Partial<ElFormLayoutItemProps>, 'label' | 'labelWidth' | 'labelConfig'>
}

// 分组项(type: 'group' 是必须的)
interface EllGroupSchema {
  type: 'group'
  /**
   * 分组 ID,必填。
   * 用于构造 portal 插槽名:
   *   分组外部:${uid}-${field}-prepend / append
   *   el-form-group 插槽:${uid}-${field}-group-desc / group-extra
   *   分组内部主体:${uid}-${field}-body-prepend / body / body-append
   */
  field: string
  label?: string

  /**
   * 分组标题旁的描述信息(区别于 EllFieldSchema 中的 description)。
   * 显示在 el-form-group 标题旁边。
   */
  description?: string

  /**
   * 分组内的表单字段(可选)。
   * 有 children → 渲染 el-form-layout 包裹这些字段;
   * 无 children → body 由 render 函数或 portal 注入。
   */
  children?: EllFieldSchema[]

  /**
   * 自定义分组内容(render 函数方式)。
   * 优先级:render > children > portal 注入(${uid}-${field}-body)
   */
  render?: (payload: { uid: string; schema: EllGroupSchema }) => VNodeChild

  /**
   * 布局配置,透传给分组内的 el-form-layout。
   * 同名属性内层(layoutConfig 内)覆盖外层(el-form-builder 全局配置)。
   */
  layoutConfig?: Partial<ElFormLayoutProps>

  groupConfig?: EllFormGroupConfig
}

// 自定义内容项(type: 'custom')
interface EllCustomSchema {
  type: 'custom'
  /**
   * 自定义块 ID,必填。
   * 用于构造 portal 插槽名:${uid}-${field}-prepend / append
   */
  field: string

  /**
   * 自定义内容渲染(render 函数方式)。
   * 与 EllGroupSchema.render 不同:custom 没有 el-form-group 包裹。
   */
  render?: (payload: { uid: string; schema: EllCustomSchema }) => VNodeChild

  /** **表单项 + 栅格**占位(custom 块外层 **`ElFormLayoutItem`**;与字段项顶层 **`formItemConfig`** 区分命名,因 custom 可含 **`label` / `labelConfig`**) */
  layoutFormItem?: Partial<ElFormLayoutItemProps>
}

// 联合类型
type EllFormSchema = EllFieldSchema | EllGroupSchema | EllCustomSchema

// 工具函数
export function isGroupSchema(schema: EllFormSchema): schema is EllGroupSchema {
  return schema.type === 'group'
}

export function isCustomSchema(schema: EllFormSchema): schema is EllCustomSchema {
  return schema.type === 'custom'
}

export function isFieldSchema(schema: EllFormSchema): schema is EllFieldSchema {
  return !schema.type || schema.type === 'field'
}

实现位置(与本节对齐): EllFormLayoutItemLabelConfigEllFormSchema 联合isGroupSchema / isCustomSchema / isFieldSchemaparseSchema 等均定义在 @element-plus-lab/utilstypes/form-layout-label.tstypes/form-schema.ts)。EllFieldSchema 仍为查询/渲染栈已落地的字段子集(Omit 键见该文件);EllFormFieldLabelConfig 等为完整 §3.1 预留。element-plus-lab 主包对白名单 export type 再导出联合与各子类型,运行时 parseSchema 请自 @element-plus-lab/utils 引用。

3.2 parseSchema:混合 Schema 处理

将扁平的 schema 数组处理为渲染单元序列。核心规则:

  • 相邻散落字段合并为同一个 el-form-layout 渲染块
  • 分组项各自独立,不与相邻分组合并
  • 处理后的序列保持原始顺序
ts
type ParsedBlock =
  | { type: 'fields'; items: EllFieldSchema[] }  // 一组相邻散落字段
  | { type: 'group';  item: EllGroupSchema }     // 单个分组

/**
 * 将混合 schema 数组解析为渲染块序列。
 *
 * 输入示例:
 *   [field-A, field-B, group-1, field-C, group-2, group-3, field-D]
 *
 * 输出示例:
 *   [
 *     { type: 'fields', items: [field-A, field-B] },
 *     { type: 'group',  item: group-1 },
 *     { type: 'fields', items: [field-C] },
 *     { type: 'group',  item: group-2 },
 *     { type: 'group',  item: group-3 },
 *     { type: 'fields', items: [field-D] },
 *   ]
 */
type ParsedBlock =
  | { type: 'fields'; items: EllFieldSchema[] }
  | { type: 'group';  item: EllGroupSchema }
  | { type: 'custom'; item: EllCustomSchema }

/**
 * 将混合 schema 数组解析为渲染块序列。
 *
 * 规则:
 * - 相邻 field 项合并为同一个 fields 块(共用一个 el-form-layout)
 * - group 和 custom 各自独立成块,不与相邻项合并
 *
 * 输入:[field-A, field-B, group-1, custom-X, field-C, group-2]
 * 输出:
 *   { type:'fields', items:[field-A, field-B] }
 *   { type:'group',  item: group-1 }
 *   { type:'custom', item: custom-X }
 *   { type:'fields', items:[field-C] }
 *   { type:'group',  item: group-2 }
 */
export function parseSchema(schema: EllFormSchema[]): ParsedBlock[] {
  const blocks: ParsedBlock[] = []
  let currentFields: EllFieldSchema[] = []

  const flushFields = () => {
    if (currentFields.length) {
      blocks.push({ type: 'fields', items: [...currentFields] })
      currentFields = []
    }
  }

  for (const item of schema) {
    if (isGroupSchema(item)) {
      flushFields()
      blocks.push({ type: 'group', item })
    } else if (isCustomSchema(item)) {
      flushFields()
      blocks.push({ type: 'custom', item })
    } else {
      currentFields.push(item as EllFieldSchema)
    }
  }

  flushFields()
  return blocks
}

四、Portal 设计

4.1 Portal 插槽命名规范

所有 portal 名称都以组件实例的 uid 为前缀,避免多实例冲突。

分组级 Portal

层次示意:

[${uid}-${field}-prepend]        ← 分组外部上方(当前分组容器之前)
el-form-group
  标题 + [${uid}-${field}-group-desc]   ← el-form-group 的描述插槽
         [${uid}-${field}-group-extra]  ← el-form-group 的额外操作插槽(标题行右侧)
  ├─ [${uid}-${field}-body-prepend]     ← 内部主体顶部(表单第一行上方,如 banner)
  ├─  表单字段 / render / body portal
  └─ [${uid}-${field}-body-append]      ← 内部主体底部(表单最后一行下方)
[${uid}-${field}-append]         ← 分组外部下方(当前分组容器之后)
插槽名位置说明
${uid}-${field}-prepend分组外部上方整个分组容器之前,用于分组间插入内容
${uid}-${field}-group-desc分组标题旁描述区对应 el-form-groupdescription 插槽,可替代 groupConfig.description 实现富文本描述
${uid}-${field}-group-extra分组标题行右侧对应 el-form-groupextra 插槽,用于在标题行放置操作按钮等
${uid}-${field}-body-prepend分组内部主体顶部表单第一行上方,适合插入 banner、提示条等
${uid}-${field}-body分组内部主体当无 children 且无 render 时,内容完全由此 portal 注入
${uid}-${field}-body-append分组内部主体底部表单最后一行下方
${uid}-${field}-append分组外部下方整个分组容器之后,用于分组间插入内容

操作按钮 Portal

按钮的 to 目标由用户通过 actions 配置传入,组件只负责发出,不预设接收位置:

ts
interface EllActionConfig {
  /** action 标识 */
  key: 'submit' | 'save' | 'reset' | string

  /** portal 目标名(用户定义的 portal-target name) */
  to: string

  /** 按钮文字 */
  text?: string

  /** 按钮 props(透传给 el-button) */
  buttonProps?: Record<string, any>

  /** 自定义渲染(优先级高于默认按钮) */
  render?: () => VNodeChild
}

字段级 Portal(el-form-builder 内)

⚠️ 职责待明确,暂不实现,保留记录供后续讨论。

| 插槽名 | 位置 ||--------|------|| ${uid}-${field}-prepend | 字段前 || ${uid}-${field}-append | 字段后 || ${uid}-${field}-prefix | 控件前缀 || ${uid}-${field}-suffix | 控件后缀 |

4.2 uid 生成

ts
import { ulid } from 'ulid'

// 在组件 setup 中
const uid = ulid()
// 通过 provide 向下传递,子组件(如 el-form-group)可以 inject 使用
provide(ELL_FORM_UID_KEY, uid)

4.3 操作按钮 Portal 使用示例

组件内部(el-form-builder)

vue
<!-- el-form-builder 内部 -->
<template v-for="action in resolvedActions" :key="action.key">
  <portal :to="action.to">
    <!-- 自定义 render -->
    <component v-if="action.render" :is="action.render" />
    <!-- 默认按钮 -->
    <template v-else>
      <el-button
        v-if="action.key === 'submit'"
        type="primary"
        @click="handleSubmit"
      >
        {{ action.text ?? '提交' }}
      </el-button>
      <el-button
        v-else-if="action.key === 'save'"
        type="primary"
        @click="handleSave"
      >
        {{ action.text ?? '保存' }}
      </el-button>
      <el-button
        v-else-if="action.key === 'reset'"
        @click="handleReset"
      >
        {{ action.text ?? '重置' }}
      </el-button>
    </template>
  </portal>
</template>

用户侧(页面)

vue
<!-- 页面中,头部接收保存按钮 -->
<el-simple-page-header title="供应商入库申请">
  <template #extra>
    <portal-target :name="`${formUid}-toolbar-save`" />
  </template>
</el-simple-page-header>

<!-- el-form-builder,配置 actions -->
<el-form-builder
  :uid="formUid"
  :schema="schema"
  :actions="[
    { key: 'save',  to: `${formUid}-toolbar-save` },
    { key: 'reset', to: `${formUid}-toolbar-reset` },
  ]"
/>

注意uid 需要在父组件 setup 里生成并同时传给 <el-form-builder><el-simple-page-header> 侧的 portal-target,保证两者使用同一个 uid。


五、config-provider(ElLabConfigProvider)

5.1 Props(当前实现)

ts
interface ElLabConfigProviderProps {
  grid?: EllGridPresetName | EllGridLayoutConfig
}

(历史方案中的更多字段可作为后续扩展,以代码为准。)

5.2 Inject Keys

ts
export const CONFIG_PROVIDER_INJECTION_KEY: InjectionKey<
  ComputedRef<ConfigProviderState>
> = Symbol('element-plus-lab:config-provider')

六、断点系统

6.1 内置断点预设

ts
export const BREAKPOINT_PRESETS = {
  'element-plus': { xs: 0, sm: 768, md: 992, lg: 1200, xl: 1920 },
  'bootstrap':    { xs: 0, sm: 576, md: 768, lg: 992,  xl: 1200, xxl: 1400 },
  'antd':         { xs: 0, sm: 576, md: 768, lg: 992,  xl: 1200, xxl: 1600 },
} as const

6.2 ColCount 类型

ts
type ColCountProp = number | Partial<Record<string, number>>

const DEFAULT_COL_COUNT: ColCountBreakpoints = {
  /** lg 起 4 列:断点按容器 clientWidth;视口 1920 减去外层 padding 后常仍小于 xl 阈值 1920,故 lg 也设为 4 */
  xs: 1, sm: 2, md: 3, lg: 4, xl: 4,
}

6.3 useBreakpoint

ts
export function useBreakpoint(options: {
  colCount?: ColCountProp
  breakpoints?: BreakpointPreset | BreakpointConfig
}) {
  // setup 阶段同步计算初始值,避免首帧闪烁
  const currentColCount = ref(calcInitialColCount())

  // ResizeObserver 监听容器宽度变化
  // window.innerWidth 匹配 EP 官方断点语义

  return { currentColCount }
}

七、Grid 布局核心(use-grid-layout.ts)

7.0 与组件的关系

  • el-form-layoutuseGridLayout(containerRef, { grid, fixedColCount? }) 输出 currentColCountgridStyle(与 ElFormQuery 同源工具)。
  • el-form-query作为子组件使用 el-form-layout,但复用 useGridLayout(含 props.fixedColCount,与 ElFormLayout.fixedColCount 语义一致);折叠 + dense + 操作区占末行末列useFormQueryGrid + simulate-dense-fill.ts 完成,不塞进通用 el-form-layout

7.1 查询折叠核心算法(目标实现,与参考代码对齐)

折叠时simulateDenseFillCollapsed(fieldColSpans, colCount, maxRows)defaultRowsNumber 作为 maxRows

  • 可用字段格为 maxRows * colCount - 1右下角一格固定为操作区;装不下的项在 fieldCells 中为 null,对应表单项 display: none(由 useFormQueryGrid.fieldStyle 打样式)。
  • 未传 defaultRowsNumber:不启用折叠网格,始终按展开态排布,且不展示「展开 / 收起」按钮。

窄屏单列(已实现):当 R × C - 1 < 1(例如响应式 colCount === 1maxRows === 1)时,若不做处理则无任何字段格,折叠态会只剩操作按钮。simulateDenseFillCollapsed 在装箱前 while (R * C - 1 < 1) R++ 增加行数,使至少保留一个字段槽,操作区落在下一行,从而满足「一列表单 + 按钮换行」。

展开时simulateDenseFillExpanded(与 dense 等价的先行后列首个可放位)+ 显式 grid-row / grid-column

  • 操作区落在首个空列后,横向合并该行剩余列,再在合并区域内右对齐;forceWrap 时操作区独占字段下方一整行。

7.2 返回值(实现映射)

方案字段当前实现位置
gridTemplateColumns / gridAutoFlowuseFormQueryGridgridContainerStyle(折叠 row + 显式行高;展开 row + 全量显式落位)
actionStyleuseFormQueryGridactionStyle(折叠右下角格;展开由 simulateDenseFillExpanded 给出合并列范围 + justify-content: flex-end
syncLayoutcolCount / schema / collapsed 的响应式依赖隐式触发,无需单独命令式 API(后续若需可再包一层 recompute
needToggle已传 defaultRowsNumbersimulateDenseFillCollapsed().hasOverflow
currentColCountuseGridLayoutResizeObserver + grid.breakpoints;可选 fixedColCount 固定列数)
ts
// 方案中的聚合形态(逻辑上等价,非单一函数返回值)
{
  gridTemplateColumns,
  gridAutoFlow,
  actionStyle,
  syncLayout,
  needToggle,
  currentColCount,
}

7.3 操作区对齐(实现约定)

  • 折叠态:操作区对应 simulateDenseFillCollapsed右下角单格cellToGridStyle)。容器为横向 flex,justify-content: flex-end重置 / 查询 / 展开 在格内靠右排列;flex-wrap: wrap(窄格时可换行)。
  • 展开态:操作区 grid-row / grid-columnsimulateDenseFillExpanded 给出——默认先在末行从首个空列横向合并至行尾colSpan = C - col),再在该合并区域内右对齐;容器 width: 100%justify-self: stretchjustify-content: flex-endforceWrap === true 时操作区为字段下方新行且占满列宽,仍整行内 flex-endflex-wrapwrap,默认 false 时为 nowrap(按钮不换行)。

八、el-form-layout + el-form-layout-item

与仓库实现一致(packages/components/form-layout):外层 grid + useGridLayout,内层 el-form-item 合并 labelConfig / formItemConfig。用户文档见 /components/basic/form-layout

8.1 el-form-layout Props

ts
interface ElFormLayoutProps {
  /** 断点列数 + 可选 colGap/rowGap;未传则继承 ElLabConfigProvider,再无则内置 element-plus 预设 */
  grid?: EllGridPresetName | EllGridLayoutConfig
  /** 固定列数(≥1):不随容器变;不传则按 grid.breakpoints + ResizeObserver 解析列数 */
  fixedColCount?: number
}

8.2 el-form-layout-item Props

ts
interface EllFormLayoutItemLabelConfig {
  labelWidth?: number | string
  /** left / right / top / '' 透传 el-form-item;justify 映射为 left + 组件内字间撑满 */
  labelPosition?: 'left' | 'right' | 'top' | 'justify' | ''
  autoLabelWidth?: boolean
  /** 仅允许 true;与 labelOverflowType 并存时以 labelOverflowType 为准;仅 true 时等价 wrap */
  labelOverflow?: true
  /** 需非 autoLabelWidth 且非 justify;语义与 ElFormQuery.labelOverflowType 一致 */
  labelOverflowType?: 'wrap' | 'nowrap' | 'ellipsis'
}

interface ElFormLayoutItemProps {
  span?: number
  gridColumn?: string
  label?: string
  prop?: FormItemProp
  rules?: Arrayable<FormItemRule>
  message?: string
  labelConfig?: EllFormLayoutItemLabelConfig
  /** 其余 el-form-item 属性(camelCase);与 labelConfig 冲突时 labelWidth/labelPosition 以 labelConfig 为准 */
  formItemConfig?: Record<string, unknown>
  class?: unknown
  style?: StyleValue
}

8.3 标签溢出与 justify(与 ElFormQuery 对齐要点)

策略要点
ellipsis启用后对本项始终应用单行省略样式;label-row 限制 max-width:100% + min-width:0,避免插槽外层被文案撑开导致 text-overflow: ellipsis 不触发;溢出时 el-tooltip 可看全文。
wrap实测溢出项加高行类;自动换行、不使用 line-clamp 截断为 「…」
nowrap在已配置 labelWidth 时不再向 el-form-item 传固定 label-width,标签随内容伸展,内容区收缩。
justifyellipsis / wrap / nowrap 互斥场景同 form-query:勿与 autoLabelWidth、溢出策略混用。实现:标签插槽内逐字 span + 内层 display:flex; justify-content:space-between.ell-form-layout-item__label-row--justifywidth/max-width:100%,使字间撑满以 Element Plus 标签列宽 为基准。文案较长时须保证 labelWidth 不小于逐字 flex:0 0 auto最小总宽,否则视觉上仍会挤出内层(flex 行为,非未生效)。

8.4 Slots

ts
// el-form-layout
slots: {
  default: ()          // 放置 el-form-layout-item
}

// el-form-layout-item
slots: {
  default: ()          // 控件内容
  label: ()            // 自定义标签(自定义时不走内置 justify/ellipsis 插槽)
  error: (scope)      // 同 el-form-item#error
}

九、el-form-renderer

9.1 控件注册

ts
interface EllControlItem {
  component: Component
  defaultProps?: Record<string, unknown>
  modelProp?: string
  modelEvent?: string
  previewComponent?: Component
  formatPreview?: (value: unknown, schema: EllFieldSchema) => string
}

export function registerControl(type: string, item: EllControlItem): void

内置 controlType(懒注册;可被 registerControl 同名覆盖):inputtextareainput-numberswitchcheckboxselectcontrolConfig.options{ label, value }[]);日期时间:date-pickerdatetime-pickerdaterange-pickertime-pickertime-select(均为 Element Plus 对应组件,v-modelmodelValue)。

9.2 渲染模式

ts
type EllFormRenderMode = 'edit' | 'disabled' | 'preview'
模式渲染方式
edit正常控件
disabled控件 + 合并 disabled: true
previewpreviewComponentformatPreview → 内置按类型纯文本;不注册控件 ref

9.3 Props

ts
interface ElFormRendererProps {
  schema: EllFieldSchema
  modelValue?: unknown
  fieldRegistryKey?: string
  mode?: EllFormRenderMode  // 默认 'edit'
}

控件透传见 schema.controlConfig(与 EllControlItem.defaultProps 合并)。

9.4 事件增强(EllFormRendererEventPayload

  • ElFormBuilder / ElFormQuery 向子树 provide FORM_RENDERER_ENHANCE_CONTEXT_KEY@element-plus-lab/utils / 主包同名导出):model()schema()getSchemaByField(field)
  • 字段 schema.events(如 change / blur / update:modelValue)与 controlConfig 中的 onXxx / onUpdate:modelValue 在挂载到子组件时,若为函数,则包装为 handler(...原参数, payload)
  • payloadEllFormRendererEventPayload{ model, schema, options, control, getSchema };其中 schemamodel 与 provide 同源,建议父级 schema 使用 reactive([...]) 以便在回调或 getSchema 中取到可改写的字段项引用;getSchemagetSchemaByField 等价。
  • schema.options:静态 { label, value }[],或 (ctx) => [] / async (ctx) => []ctxEllFieldSchemaOptionsContext{ schema, model, control }schema 为整表 EllFormSchema[],与 provide 同源)。select 会解析后覆盖 controlConfig.optionspayload.options同步快照(async 在事件触发瞬间可能仍为 [])。
  • defineExposegetSchemaByFieldgetSchema(别名),与 getControlReffield 语义对齐。
  • 无 provide(单独使用 ElFormRenderer)时:payload.model 仅含当前 field 键;payload.schema[当前项]getSchema 仅能命中当前字段。

十、el-form-query

10.0 实现要点

  • 对外封装 el-form,表单项为 el-form-item + el-form-renderer
  • 在内部使用 el-form-layout;布局为 query 专用实现,以便接入 dense 折叠 并使 操作按钮在折叠态下占据末行末列(见 §二、§七)。
  • 已实现 §7.1:折叠 / 展开均按 schema 的 span(默认 1)做列合并装箱;折叠simulateDenseFillCollapsed + 右下角操作格(必须传入 defaultRowsNumber 才启用折叠 UI);展开simulateDenseFillExpandedspanElFormLayoutItemProps.span 语义一致(见 §三 EllFieldSchema、§8.2)。操作区右对齐与列合并§7.3§10.2

10.1 使用方式与布局

配置方式el-form-query 只支持通过 schema 配置表单项,不提供在组件内用默认插槽手写 el-form-item 的用法(与 el-form-renderer / 控件注册表配合渲染)。

布局方式(列数 / 断点 + §7.1 折叠 dense):

  • 列合并:schema 项上的 span(与 el-form-layout-itemspan 同语义)指定占几列;折叠、展开两种布局均参与装箱。
  • 网格配置(已实现):使用 grid 传命名预设(element-plus / bootstrap / antd)或自定义 EllGridLayoutConfigbreakpoints: { name, min, cols }[]);亦可由 ElLabConfigProvider 注入默认 grid。单档固定列数可写 :grid="{ breakpoints: [{ name: 'all', min: 0, cols: 3 }] }",或与 ElFormLayout 一致使用 fixed-col-countprops.fixedColCount),列数不随容器变、colGap / rowGap 仍来自 gridcol-gap / row-gap / compact 覆盖。(历史方案中的独立 colCount 已收敛为 grid + 可选 fixedColCount。)
vue
<!-- 响应式列数(断点) -->
<el-form-query
  v-model="query"
  :schema="querySchema"
  :grid="{ breakpoints: [{ name: 'all', min: 0, cols: 3 }] }"
  @search="load"
/>
vue
<!-- 固定 4 列(与 ElFormLayout 对齐) -->
<el-form-query
  v-model="query"
  :schema="querySchema"
  :fixed-col-count="4"
  @search="load"
/>

10.2 操作区对齐与列合并

  • 按钮顺序(从左到右):重置 → 查询 → 展开/收起(与 DOM 顺序一致;在操作区内整体靠右)。实现上三个默认按钮经 portal-vue 投递到同一 PortalTarget(名称见 §10.6),通过 order 与自定义投递内容混排;5 / 10 / 15 由组件占用,自定义内容勿使用这三个 order
  • 折叠:操作区只占折叠网格最后一格(右下角),按钮在该格内 justify-content: flex-end 右对齐。
  • 展开(默认 force-wrap=false:先用 simulateDenseFillExpanded当前行从第一个空列到最后一列合并为操作区占位,再在该区域内右对齐;避免「只占一列却在列内靠右」导致左侧大段空白未并入操作区的问题。
  • 展开(force-wrap=true:操作区在所有字段之后另起一行并占满列宽,仍在该行内右对齐;按钮允许换行。
  • 宽度min-width 作用于表单根节点,小于父级宽度时由外层 .ell-form-query__viewport 横向滚动;max-width 封顶宽度并 margin: 0 auto 整体居中,不再随父级变宽而拉伸。

10.3 Props

ts
interface ElFormQueryProps {
  /** 默认 **`v-model`**(`modelValue`) */
  modelValue: Record<string, any>
  /** 查询项配置(必填);仅支持 schema 驱动渲染 */
  schema: EllFieldSchema[]
  /** 实例 id;未传则内部生成。与操作区 **`PortalTarget`** 名称 **`` `${uid}-actions` ``** 同源(见 §10.6) */
  uid?: string
  grid?: EllGridPresetName | EllGridLayoutConfig
  /** 固定列数(≥1);不传则按 grid 断点响应式解析,语义同 ElFormLayout.fixedColCount */
  fixedColCount?: number
  colGap?: number | string
  rowGap?: number | string
  collapsed?: boolean            // 受控
  defaultCollapsed?: boolean
  /** 未传则不启用折叠网格,且不显示「展开/收起」 */
  defaultRowsNumber?: number
  /** 仅当已传 `defaultRowsNumber` 且字段溢出时生效 */
  showCollapseToggle?: boolean
  /**
   * 展开态:`false`(默认)末行合并剩余列后与字段同排,操作区内右对齐,按钮 `nowrap`;
   * `true` 时操作区独占字段下方整行并右对齐,按钮 `wrap`。
   */
  forceWrap?: boolean
  showSearch?: boolean
  searchText?: string
  showReset?: boolean
  resetText?: string
  /** `true` 时绑定内置「查询」按钮的 **`loading`**,防止重复点击 */
  loading?: boolean
  /** 若传入:校验通过后改为调用本函数,**不** `emit('search')`(含 `immediate` / `immediateDebounce`) */
  searchHandler?: (model: Record<string, unknown>, formRef: FormInstance | undefined) => void
  /** 若传入:`resetFields()` 之后改为调用本函数,**不** `emit('reset')` */
  resetHandler?: (model: Record<string, unknown>, formRef: FormInstance | undefined) => void
  /** 若传入:折叠态变更时改为调用本函数,**不** `emit('collapseChange')` */
  toggleHandler?: (collapsed: boolean) => void
  /** 透传 Element Plus `el-form` `scroll-to-error` */
  scrollToError?: boolean
  /** 透传 `el-form` `scroll-into-view-options` */
  scrollIntoViewOptions?: ScrollIntoViewOptions | boolean
  /** 挂载后 nextTick 触发一次 `search`(载荷同手动查询) */
  immediate?: boolean
  /** 正数:对 model 深度 watch,防抖毫秒后触发 `search` */
  immediateDebounce?: number
  labelWidth?: number | string
  labelPosition?: 'left' | 'right' | 'top' | 'justify'
  autoLabelWidth?: boolean
  /** 仅允许字面量 `true`;与 `labelOverflowType` 配合见 §10.3.1 */
  labelOverflow?: true
  labelOverflowType?: 'wrap' | 'nowrap' | 'ellipsis'
  compact?: boolean
  /** 小于父级可用宽度时外层横向滚动;数字为 px */
  minWidth?: number | string
  /** 宽度封顶并在父级内水平居中 */
  maxWidth?: number | string
  size?: 'large' | 'default' | 'small'
  disabled?: boolean
}

校验失败滚动:不单独实现「滚到第一项错误」;沿用 Element Plus el-formscroll-to-error / scroll-into-view-options,通过上表 props 透传配置即可。

标签与密度(已实现)label-width / label-position / size / disabled 透传根节点 el-form(与 Element Plus 一致);auto-label-widthtrue 时等价 label-width="auto"(优先于数值 label-width);compact 在未单独传 col-gap / row-gap 时把网格间距默认改为 8px,并为标签加上略小字号样式。label-position="justify"el-form 仍为 left;标签逐字span,外层 flex + justify-content: space-between 撑满标签宽(见 掘金:表单标签两端对齐)。

自动查询(已实现)immediatetrue 时挂载后 nextTick 触发一次 search(载荷同手动查询);immediateDebounce正数毫秒时对 v-model(表单对象)深度监听,变更后防抖触发 search。二者可组合(首屏拉数 + 改条件防抖请求)。overflowBehavior 已从本接口移除(未纳入实现路线)。

10.3.1 标签溢出 labelOverflow / labelOverflowType(已实现)

固定 labelWidth autoLabelWidth labelPosition: 'justify' 时,可通过 labelOverflowType 启用策略(单独传入即生效);labelOverflow?: true 且未传 labelOverflowType 时等价 wrap。勿使用可选 boolean false 表达「关闭」——Vue 会与「未传」混淆;关闭时请不传 labelOverflowType

取值行为要点
wrap仅对实测会溢出的表单项打标;自动换行、无尾部省略号line-height: 1.25;溢出项用 flex align-items: center 与控件垂直对齐。网格内所有 .el-form-item__content 统一 column + align-items: flex-start;含 textarea 的项表单项行内亦为 flex-start。未溢出项不套 wrap 样式。测量在 requestAnimationFrame + 容器 ResizeObserver 下重算。
nowrap已向 el-form 传固定 label-width 时改为不传,标签按内容伸展,内容区 flex:1; min-width:0 收缩。
ellipsis启用 labelOverflowType="ellipsis" 时,所有字段应用单行省略与 label-row 宽度约束(避免 Tooltip 触发器撑开宽度导致无 );el-tooltip 仅在该项实测溢出时启用。

renderLabel 字段不参与自动测量与打标(自定义标签自行处理)。

10.4 Events / Expose

ts
emit('search', model)
emit('reset')
emit('collapseChange', collapsed)
// v-model:collapsed 由 defineModel 提供 update:collapsed

事件与命令式回调:若传入 searchHandler / resetHandler / toggleHandler 中任一,则对应操作不再 emit search / reset / collapseChange,由回调完全接管(immediateimmediateDebounce 触发的查询同样走 searchHandler)。

ts
expose({
  model,         // 与 v-model 同源(见 §11.5.1 与 ElFormBuilder 对齐的 ref API)
  formRef,       // Element Plus FormInstance
  uid,           // 解析后的实例 id(与 props.uid 或自动 ulid 一致),与操作区 Portal 名称同源
  collapsed,     // 与 v-model:collapsed 同步
  needToggle,
  toggleCollapse,
  currentColCount,
  getControlRef, // 同 §11.5.1:`EllFormBuilderFieldRefSpec` → 控件实例(`ElFormRenderer` 注册的根控件)
  getFormItemRef, // 同 §11.5.1:`el-form-item` 包装实例(模板 `ref` 回调注册)
  formItemRef,   // Proxy:点分键读取表单项,与 `getFormItemRef(spec)` 归一化键一致
})

10.5 插槽

插槽作用域参数说明
默认插槽(无 nameuidactionsPortalName</el-form> 之后、内置三个操作按钮 <portal> 之前插入(仍在 ElFormQuery 根内)。便于作为子节点书写 <portal :to="actionsPortalName" :order="…">,与内置按钮共用同一 PortalTarget最终左右顺序由 order 决定,与插槽相对 DOM 先后无关。模板中作用域属性 actions-portal-name(kebab) 解构为 actionsPortalName
actionsmodel(与 v-model 同源)、formRefel-form 实例 ref)整块操作区由用户完全自定义;组件只在网格中留出操作格,并通过作用域参数提供 modelformRef,便于自行排版按钮并做校验、重置、触发自定义查询等。未覆盖该插槽时,默认内容为 PortalTarget`${uid}-actions`multipleslot-props),与 §10.6 内置 <portal>(重置 / 查询 / 展开收起)及 #default 里自定义 portal 共用同一落点并按 order 混排。

10.6 portal-vue 操作区(已实现)

  • 单一目标名`${uid}-actions`uid§10.4 中解析结果,与根节点 data-ell-form-query-uid 一致)。所有操作区按钮(含内置与自定义)均 <portal :to="…"> 到该名称
  • 排序:在同一 PortalTarget 上依赖 portal-vue 的 order(数值越大越靠后,与 flex order 语义一致)。组件内置三个入口固定为:
    • order: 5 — 重置(showReset
    • order: 10 — 查询(showSearch;先 validatesearch
    • order: 15 — 展开/收起(showCollapseToggle && needToggle
  • 自定义:仅可使用 除 5、10、15 以外order(例如 1–4 放在重置前,6–9 在重置与查询之间,11–14 在查询与展开之间,≥16 在展开之后),避免与内置按钮覆盖或顺序错乱。
  • 查询 / 重置 / 折叠:若在操作区外自定义按钮,需自行调用 ElFormQueryexpose(如 formRef.validate() + emit('search')searchHandler 等价逻辑formRef.resetFields()toggleCollapse)或继续使用向 ${uid}-actionsportal 投递。

历史草案中曾列举 action-prefix / action-suffix 等插槽名,已废弃;以 §10.5、§10.6#default / #actions + PortalTarget + order)为唯一约定。

凡使用 portal-vue 与表单、工具栏等对齐时,uid 须在共同父级统一生成,并与各 portal-targetnameel-form-query / el-form-builder 实例约定一致。

十一、el-form-builder

11.0 定位(与 el-form-query 的差异)

  • 操作区不提供内置「提交 / 保存 / 重置」等按钮;由用户通过 actions 配置 portal :to="…",并在布局中显式放置同名 portal-target(页面头、抽屉脚等),无默认落点
  • loading提供专用于表单提交防重按的 loading prop(与 el-form-query 仅绑定「查询」按钮的 loading 区分);页面级加载由头部等兄弟组件自行处理。
  • 校验失败滚动:不在 el-form-builder 层重复实现;在透传el-form 的配置(如业务里的 formConfig)中使用 Element Plus 自带的 scroll-to-error / scroll-into-view-options 即可。
  • 已实现 · 结构parseSchema 驱动 fields / group / custom 块序渲染;group 使用 ElFormGroup + 可选 groupConfig,子内容为 children → 内层 ElFormLayout,或与文档一致优先 rendercustom 外层为 ElFormLayout + ElFormLayoutItemv-bind="layoutFormItem",内层走 render${uid}-${field}-body 占位fields / 分组子字段通过 ElFormLayoutItem 消费 label / labelWidth / labelConfig(含 description / renderLabelformItemConfig(与 §3.1 合并规则一致)。uid?** 未传时 ulid()(与 query 同策略)。defineExpose({ mergedGridLayout, parsedBlocks, uid, formRef, formHeaderPortalName, formFooterPortalName })
  • 已实现 · portal-vueel-form 内首、尾为 ${uid}-form-header${uid}-form-footer(表级注入;EllFieldSchema 所在 fields 块不在前后挂 portal)。group / custom${uid}-${field}-…(prepend、append 及分组 group-desc、group-extra、body-* 等,见 §11.1)。各 PortalTarget slot-props{ model, formRef, uid }。根 #default 插槽透出 uid / formRef / model / formHeaderPortalName / formFooterPortalName(后两者为目标名字符串,与 defineExpose 同源)。

11.1 分组渲染流程(含 custom)

schema(混合数组)
    ↓ parseSchema()
ParsedBlock 序列
    ↓ 渲染(el-form 内)
┌─────────────────────────────────────────────────────────────┐
│  <portal-target :name="`${uid}-form-header`" />            │
├─────────────────────────────────────────────────────────────┤
│  block: 'fields'(无块级 portal;字段 schema 不定义前后插入)│
│  → <el-form-layout :layoutConfig="mergedLayoutConfig">      │
│      <el-form-layout-item                                   │
│        v-for="field"                                        │
│        v-bind="fieldItemProps(field)"  ← label/labelConfig/ │
│          formItemConfig 合并(§3.1)                         │
│      />                                                     │
├─────────────────────────────────────────────────────────────┤
│  block: 'group'                                             │
│  → <portal-target :name="`${uid}-${field}-prepend`" />     │
│  → <el-form-group :title="label">                          │
│      description slot:                                      │
│        <portal-target :name="`${uid}-${field}-group-desc`"/>│
│      extra slot:                                            │
│        <portal-target :name="`${uid}-${field}-group-extra`"/>│
│      body(优先级:render > children > portal):           │
│        <portal-target :name="`${uid}-${field}-body-prepend`"/>│
│        render()                                             │
│        OR <el-form-layout v-bind="group.layoutConfig">      │
│             + children 字段(复用已有 el-form-group 组件)  │
│        OR <portal-target :name="`${uid}-${field}-body`" /> │
│        <portal-target :name="`${uid}-${field}-body-append`"/>│
│  → <portal-target :name="`${uid}-${field}-append`" />      │
├─────────────────────────────────────────────────────────────┤
│  block: 'custom'                                            │
│  → <portal-target :name="`${uid}-${field}-prepend`" />     │
│  → <el-form-layout>                                         │
│       <el-form-layout-item v-bind="layoutFormItem">         │
│         render()                                            │
│         OR <portal-target :name="`${uid}-${field}-body`"/> │
│       </el-form-layout-item>                                │
│     </el-form-layout>                                       │
│  → <portal-target :name="`${uid}-${field}-append`" />      │
├─────────────────────────────────────────────────────────────┤
│  <portal-target :name="`${uid}-form-footer`" />             │
└─────────────────────────────────────────────────────────────┘

整表顶部、底部插入(与 **fields 块无关):

${uid}-form-header
${uid}-form-footer

uidprops.uid 或内部 ulid() 决定,并经 #default 插槽与 defineExpose 一并透出 formHeaderPortalName / formFooterPortalName(字符串,等于上表带真实 uid 的完整名)。

11.3 Props(梳理稿 · 与 el-form-layout / el-form-query 对齐)

以下为 实现前 的属性清单:网格与标签命名与 ElFormQueryElFormLayout 保持一致(grid + fixedColCount?,不再使用已废弃的独立 colCount / breakpoints 根 props);§11.1 流程与 EllActionConfig 不变。

11.3.1 分层

分层说明
数据v-modelmodelValue)表单对象;initialValues? 仅在 reset 等场景恢复初值(实现时定稿)。
结构schema: EllFormSchema[]§3);字段项标签仅顶层 labellabelWidth,其余标签能力进 labelConfig(见 §3.1)。
实例uid?portal / actions[].to;不传则 ulid()
网格grid?fixedColCount?colGap?compact?:列间距等与 ElFormQuery / grid 合并规则一致;rowGapElFormBuilder 上默认 0继承 grid / Provider / compact 的行间距,仅 props.rowGap 可显式设置。子块用 layoutConfig 覆盖 grid / fixedColCount 等。
formConfig透传 el-form非必需大块配置scrollToErrorscrollIntoViewOptionssizedisabledlabelPositionautoLabelWidth、视口 minWidth/maxWidth、以及其它 EP el-form 文档中的 prop(camelCase)。与 labelConfig 同键时以 labelConfig 为准
labelConfig整表默认 EllFormLayoutItemLabelConfig(及扩展字段,见 §3.1 EllFormFieldLabelConfig 中仅字段级才有的 description / renderLabel);覆盖 formConfig 同名项
顶层 labelWidth唯一保留在 ElFormBuilder上的标签宽度快捷;解析优先级labelConfig.labelWidth > 顶层 labelWidth > formConfig 中的 labelWidth / label-width
模式 / 业务mode?editableFields?validateOnMount?actions?controls?*Handler?:含义同前。validateOnMount 也可放入 formConfig,若与顶层重复则 以顶层面向实现的显式约定为准(建议只放一处)。

11.3.2 formConfiglabelConfig(对齐 el-form-layout-item

  • ElFormLayoutItemformItemConfig + labelConfig 的关系类似:formConfig 对应「根级 el-formformItemConfig——打包非核心少手写的透传;labelConfig 专门收敛标签行为
  • formConfiglabelConfig:如 labelPositionautoLabelWidthlabelWidth(EP 常为 label-width)等,始终以 labelConfig 解析结果覆盖 formConfig
  • 字段级§3.1):labellabelWidthEllFieldSchema 顶层labelConfigdescriptionrenderLabelEllFormLayoutItemLabelConfig 全部;渲染 ElFormLayoutItem字段 labelConfig 优先于与表单项合并对象中的标签相关键(与 formItemConfig.formItemConfig / layoutFormItem.formItemConfig 冲突时仍遵循 ElFormLayoutItem 既有规则labelConfig 优先于内层透传中的标签相关键)。

11.3.3 类型草案

ts
/** 整表 `el-form` 透传对象(camelCase);不含 v-model、schema */
type EllFormBuilderFormConfig = Record<string, unknown>

interface ElFormBuilderProps {
  modelValue: Record<string, unknown>
  schema: EllFormSchema[]
  initialValues?: Record<string, unknown>
  uid?: string

  grid?: EllGridPresetName | EllGridLayoutConfig
  fixedColCount?: number
  colGap?: number | string
  rowGap?: number | string
  compact?: boolean

  /** 根上唯一标签宽度快捷;见 §11.3.1 优先级 */
  labelWidth?: number | string

  /**
   * 透传 `el-form`:**scroll**、**size**、**disabled**、**minWidth/maxWidth**(若实现为视口)、**labelPosition**(未在 `labelConfig` 写时可用)等。
   * 与 **`labelConfig`** 同名字段 **以 `labelConfig` 为准**。
   */
  formConfig?: EllFormBuilderFormConfig

  /** 整表默认标签行为;覆盖 **`formConfig`** 中同键 */
  labelConfig?: EllFormLayoutItemLabelConfig

  /** 子级 **`ElFormRenderer`**,见 §9.2 */
  mode?: EllFormRenderMode
  editableFields?: string[]
  validateOnMount?: boolean

  actions?: EllActionConfig[]
  controls?: EllControlRegistry

  submitHandler?: (model: Record<string, unknown>, formRef: FormInstance | undefined) => void | Promise<void>
  saveHandler?: (model: Record<string, unknown>, formRef: FormInstance | undefined) => void | Promise<void>
  resetHandler?: (model: Record<string, unknown>, formRef: FormInstance | undefined) => void
}

已从旧稿平面属性迁入 formConfig 的示例scrollToErrorscrollIntoViewOptionssizedisabledlabelPositionautoLabelWidthminWidth / maxWidthlabelConfig 负责justify 对应侧的 labelPosition: 'justify'labelOverflow* 等(待与 ElFormQuery 整表策略对齐时实现)。

11.4 EllActionConfig

ts
interface EllActionConfig {
  key: 'submit' | 'save' | 'reset' | string
  to: string                        // portal-target 的 name
  text?: string
  buttonProps?: Partial<ButtonProps>
  render?: () => VNodeChild          // 完全自定义渲染,忽略其他配置
}

内置 key 的默认行为

key默认行为
submitformRef.validate() 通过后 emit('submit', model)submitHandler(若实现互斥规则)
saveemit('save', model)saveHandler(不做校验)
resetformRef.resetFields() + 按 initialValues 恢复;emit('reset')resetHandler

11.5 Events / Expose

ts
// Events
emit('update:modelValue', model)
emit('submit', values)
emit('save', values)
emit('reset')
emit('validate-failed', errors)
emit('field-change', field, value, model)

// Expose
expose({
  uid,              // 当前实例的 uid(供外部构造 portal-target name)
  formRef,
  model,
  getControlRef,    // (spec: EllFormBuilderFieldRefSpec) => 底层控件实例;spec 与 el-form-item prop / 点分 / address[2] 等对齐
  getFormItemRef,   // (spec) => ElFormItem 实例(表单校验、错误状态等)
  formItemRef,      // Proxy:按注册键(点分)读取表单项,等价于 getFormItemRef(key)
  getSchemaByField, // (field: EllFieldSchema['field']) => 字段项引用,见 §11.5.1
  getSchema,        // 同 getSchemaByField
})

11.5.1 字段级控件 / 表单项实例(getControlRef / getFormItemRef

  • 类型EllFormBuilderFieldRefSpec(定义在 @element-plus-lab/utils,主包 export type 白名单可查)支持 string{ prop }{ segments };字符串支持 address[2]lines.1 等与 el-form-item prop 一致的写法,内部统一为 点分注册键(如 address.2)。工具函数:ellFormBuilderFieldSpecToRegistryKeyellFormItemPropToRegistryKey
  • 注入FORM_BUILDER_FIELD_REGISTRY_KEYEllFormBuilderFieldRegistry)由 ElFormBuilderElFormQuery provide同一 InjectionKey);ElFormRenderer 注册控件ElFormLayoutItemElFormQuery 内对每个 el-form-itemref 回调 注册表单项FORM_RENDERER_ENHANCE_CONTEXT_KEY§9.4
  • Schema 查找defineExpose 提供 getSchemaByField / getSchema,参数为 EllFieldSchema.field(与 model 上数据键一致),不等同getControlRef({ prop: 'lines.1' }) 所用 prop 字符串;propfield 不一致时请用 field 取 schema 项、用 prop 规格 取 ref。
  • ElFormRenderer:见 §9.3 fieldRegistryKey;在 builder 内与 fieldItemProps(item).prop 解析结果一致,保证 getControlRefgetFormItemRef lookup 同一键。
  • 覆盖范围:仅经 ElFormRenderer 渲染的字段注册 控件 refgroup / customrender、仅有 PortalTarget 等路径 getControlRefgetFormItemRef 是否可用取决于是否包在 ElFormLayoutItem + prop 下)。
  • 动态 schema:项增减后须在 nextTick(或等价的挂载后时机)再调用 getControlRef / getFormItemRef,待子树挂载并完成注册。
  • formItemRef Proxy:属性名为 点分字符串(与归一化后的 spec 一致),例如 formItemRef['lines.1']getFormItemRef('lines[1]') 等价。
  • v-modelprop:当前 ElFormBuilderfields / group 子字段 仍使用 v-model="model[item.field]"。若 formItemConfig.proplines.2field 取其它名,则 校验路径与数据绑定可能不一致,需业务将 model 形状与 prop 对齐,或 field 与绑定路径统一(后续若在 builder 内支持「按 prop 绑定」再修订本句)。

11.6 典型使用示例

vue
<script setup lang="ts">
import { ulid } from 'ulid'

const formUid = ulid()
const form = ref({})

const schema: EllFormSchema[] = [
  { field: 'name',  label: '姓名', controlType: 'input' },
  { field: 'phone', label: '手机', controlType: 'input' },

  {
    type: 'group',
    field: 'addressGroup',
    label: '地址信息',
    children: [
      { field: 'province', label: '省份', controlType: 'select' },
      {
        field: 'city',
        label: '城市',
        controlType: 'select',
        dependencies: {
          triggerFields: ['province'],
          show: values => values.province != null && values.province !== '',
        },
        options: async ctx => fetchCities(String(ctx.model.province ?? '')),
      },
    ],
  },

  // 无 children,body 完全由 render 注入
  {
    type: 'group',
    field: 'detailGroup',
    label: '明细',
    render: ({ uid, schema }) => h(MyDetailTable, { uid }),
  },

  { field: 'remark', label: '备注', controlType: 'textarea', span: 3 },
]
</script>

<template>
  <!-- 页面头部接收保存按钮 -->
  <el-simple-page-header title="入库申请">
    <template #extra>
      <portal-target :name="`${formUid}-toolbar-save`" />
    </template>
  </el-simple-page-header>

  <!-- 通过 portal 向 addressGroup 标题旁插入操作 -->
  <portal :to="`${formUid}-addressGroup-header`">
    <el-button size="small" link>从地址簿选择</el-button>
  </portal>

  <el-form-builder
    :uid="formUid"
    v-model="form"
    :schema="schema"
    :col-count="{ md: 2, lg: 3 }"
    :actions="[
      { key: 'save',  to: `${formUid}-toolbar-save`,  text: '保存草稿' },
      { key: 'reset', to: `${formUid}-toolbar-reset` },
    ]"
    :on-save="handleSave"
  />
</template>

十二、useForm 组合式函数

实现packages/components/hooks/use-form;对外 useForm 与类型见 element-plus-lab;文档见 Hooks · useForm

ElFormBuilder 之上持有 model / schemaRef 可传)/ uid,模板渲染 Form 即可;options 与其余 ElFormBuilderProps 一致(勿传 modelValue)。

ts
const {
  Form,
  uid,
  model,
  schema,
  builderRef,
  formRef,
  validate,
  validateField,
  clearValidate,
  resetFields,
  scrollToField,
  getFieldValue,
  setFieldValue,
  getFieldsValue,
  setFieldsValue,
  submit,
  save,
  setSchema,
  getControlRef,
  getFormItemRef,
  formItemRef,
} = useForm({
  schema: [...],
  initialValues: { name: '张三' },
  formConfig: { labelWidth: '100px' },
  actions: [{ key: 'submit', to: `${myUid}-actions` }],
  submitHandler: async (model, formRef) => {
    await api.save(model)
  },
})

说明:当前 EllFormSchema 不含草案中的 default / transform 等键;initialValuesElFormBuilder 一致;统一的 errors ref(请使用 validate / validateFieldel-form 能力)。updateSchema 可用自持 Ref(schema) 替换或 setSchema 全量更新。


十三、预览模式

13.1 三态渲染回落策略

自定义 render() → controlItem.previewComponent → formatter() 文本 → 原始值 toString

13.2 内置预览组件

类型预览组件说明
input/textareaEllTextPreview文本省略 + tooltip
selectEllSelectPreview展示选中 label,多选逗号连接
date-pickerEllDatePreviewformat prop 格式化
switchEllSwitchPreviewactiveText/inactiveText
rateEllRatePreview只读 el-rate
uploadEllUploadPreview文件列表 / 图片预览

十四、标签系统

14.1 autoLabelWidth

ts
function calcAutoLabelWidth(labels: string[]): number {
  const CJK = 14, ASCII = 8, PADDING = 14
  const max = Math.max(...labels.map(label => {
    let w = 0
    for (const char of label) {
      w += /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? CJK : ASCII
    }
    return w
  }))
  return Math.max(max + PADDING, 72)
}

14.2 labelPosition: 'justify'(分散对齐 · 已实现)

早期草案曾考虑纯 CSS text-align: justify当前实现ElFormQueryElFormLayoutItem)均为:根 el-form / el-form-item 仍为 label-position: left,标签插槽内逐字span,内层容器 display: flexjustify-content: space-between 撑满 label-width。样式约束见 §8.3form-query / form-layout-item 源码(__label-row--justify__justify-inner)。


十五、事件增强

enhanceEvents: true 时,控件事件回调额外接收 ctx:

ts
controlEvents: {
  change: (value, ctx) => {
    // ctx.model    — 当前表单数据
    // ctx.schema   — 完整 schema 数组
    // ctx.getField — (field: string) => EllFieldSchema
    // ctx.setValue — (field: string, value: any) => void(直接修改 model)
    // ctx.options  — 当前字段已加载的 options
  }
}

十六、Portal 注意事项

Portal 机制是本方案中最需要边走边调整的部分,以下是目前预判的关键问题:

1. portal-target 必须先于 portal 存在<portal-target> 所在组件(如 <el-simple-page-header>)必须在 <el-form-builder> 之前挂载,否则 portal 内容找不到目标。实际上 Vue 的渲染顺序通常是父组件先于子组件,页面头部先于下方表单,一般没问题。但要注意异步组件或条件渲染场景。

2. uid 需要在父组件 setup 中生成 使用 portal-vue 时,<el-form-builder>(或 query)与各处 <portal-target> 须使用同一 uid,须在共同父组件中生成,不能分拆各自生成。useForm 提供的 uid 就是为了解决这个问题。

3. 组件卸载时的清理<el-form-builder> 卸载后,其内部的 <portal> 会自动清空 target,无需手动处理。但如果 target 依然存在(页面头部还在),会看到按钮消失——这是预期行为。

4. SSR 场景 portal-vue 在 SSR 下需要额外配置,若项目有 SSR 需求需提前确认。


十七、实现优先级

Phase 1 — 基础设施
  utils: breakpoints + form-schema(含 parseSchema)+ form-builder-field-registry / field-path(`getControlRef` 注册表)
  utils: use-grid-layout + simulate-dense-fill + form-label-overflow

Phase 2 — 布局组件
  el-form-layout + el-form-layout-item(grid / fixedColCount;labelConfig 与 query 对齐)

Phase 3 — 渲染引擎
  el-form-renderer(控件注册 + 三态渲染 + v-model 修饰符)

Phase 4 — 查询表单
  el-form-query(schema 驱动;扩展位后续 portal-vue)

Phase 5 — 完整表单
  el-form-builder(parseSchema + 分组渲染 + portal actions)
  useForm

Phase 6 — 完善体验
  autoLabelWidth + compact(校验失败滚动沿用 EP `el-form` `scroll-to-error` 透传)
  transform 机制 + 预览组件完善
  portal 相关的边界情况处理

Phase 7 — 业务组件(components/business/)
  useFormDialog + useFormDrawer
  el-cru-page

十八、业务组件层

放置路径:packages/components/(与基础组件平级,统一发布) 文档路径:docs/content/components/business/

基于表单基础组件封装的业务场景复合组件,随 packages/element-plus-lab 统一对外发布。


18.1 useFormDialog()

定位:在现有 useDialog() 基础上注入 el-form-builder,适用于弹窗新建/编辑场景。

原 useDialog 结构(参考)

ts
useDialog({
  renderHeader: () => VNodeChild
  render:        () => VNodeChild
  renderFooter:  () => VNodeChild
  okHandler:     () => void | Promise<void>
  cancelHandler: () => void
})

useFormDialog 扩展

useDialog 的弹窗能力上固定内容区为 ElFormBuilder(可 render 完全自绘),并注入 model / formRef。选项类型见 UseFormDialogOptionspackages/components/form-dialog)。

ts
/** formConfig:勿传 modelValue;勿传 Builder 侧 submit/save/resetHandler(与弹层 footer 冲突) */
type EllFormDialogFormConfig = Omit<
  ElFormBuilderProps,
  'modelValue' | 'submitHandler' | 'saveHandler' | 'resetHandler'
>

interface UseFormDialogOptions {
  formConfig: EllFormDialogFormConfig
  title?: string
  width?: number
  dialogConfig?: Partial<DialogProps>
  submitText?: string
  cancelText?: string
  actions?: Array<'cancel' | 'ok'>
  renderHeader?: (payload: EllFormDialogPayload) => VNodeChild
  render?: (payload: EllFormDialogPayload) => VNodeChild
  renderFooter?: (payload: EllFormDialogPayload) => VNodeChild
  /** validate 通过后调用;再 resolve({ reason: 'ok', data: { model } }) */
  submitHandler?: (payload: EllFormDialogPayload) => void | Promise<void>
  cancelHandler?: (payload: EllFormDialogPayload) => void
  /** 与 useDialog / EllDialogProps 一致 */
  beforeClose?: EllDialogProps['beforeClose']
}

interface EllFormDialogPayload {
  model: Record<string, unknown>
  formRef: FormInstance | undefined
}

内部实现要点

  • ElFormBuilder 默认在内容槽渲染,uid / v-modeluseFormDialog 内部持有;formConfig.actions 默认 []
  • 底部「提交」:validate()submitHandler?resolve;「取消」:cancelHandler?reject({ reason: 'cancel' })
  • 另暴露 close(result?)dismiss();打开 API 为 open / openFormDialog(与 openDialog 区分命名)

使用示例

ts
const { open, close, dismiss } = useFormDialog({
  formConfig: {
    schema: [
      { field: 'name', label: '姓名', controlType: 'input', /* rules … */ },
      { field: 'email', label: '邮箱', controlType: 'input' },
    ],
    fixedColCount: 2,
    initialValues: { name: '', email: '' },
    grid: { breakpoints: [{ name: 'all', min: 0, cols: 2 }] },
  },

  renderHeader: ({ model }) =>
    h('span', `编辑用户:${String(model.name || '新用户')}`),

  submitHandler: async ({ model }) => {
    await api.saveUser(model)
    // 需要仅关窗不传 ok 快照时可自定义 close({ ... })
    close()
  },
})

await open()
open({ initialValues: { name: '张三', email: 'z@example.com' }, title: '编辑' })

对外文档:组件 · useFormDialog


18.2 useFormDrawer()

定位:与 useFormDialog 完全对称,将弹窗换为抽屉,适用于复杂编辑场景(内容较多时抽屉比弹窗体验更好)。

实现见 packages/components/form-drawerformConfig 约束与 EllFormDialogFormConfig 相同(勿传 modelValue 与 Builder 侧 submit/save/reset handler)。

ts
interface UseFormDrawerOptions {
  formConfig: EllFormDrawerFormConfig
  title?: string
  size?: number | string
  variant?: EllDrawerProps['variant']
  headerConfig?: EllDrawerProps['headerConfig']
  drawerConfig?: Partial<DrawerProps>
  /** 不可传 okHandler / cancelHandler,由钩子写入 */
  actionConfig?: Omit<EllDrawerActionConfig, 'okHandler' | 'cancelHandler'>
  submitText?: string
  cancelText?: string
  actions?: Array<'cancel' | 'ok'>
  renderHeader?: (payload: EllFormDrawerPayload) => VNodeChild
  render?: (payload: EllFormDrawerPayload) => VNodeChild
  renderFooter?: (payload: EllFormDrawerPayload) => VNodeChild
  submitHandler?: (payload: EllFormDrawerPayload) => void | Promise<void>
  cancelHandler?: (payload: EllFormDrawerPayload) => void
  beforeClose?: EllDrawerProps['beforeClose']
}

interface EllFormDrawerPayload {
  model: Record<string, unknown>
  formRef: FormInstance | undefined
}

与 useFormDialog 的差异

方面useFormDialoguseFormDrawer
容器el-dialogel-drawer
尺寸width + dialogConfigsize + drawerConfig
底部操作actions / okText 等平铺写入 actionConfigplacement 可为 top
打开 APIopen / openFormDialogopenDialogopen / openFormDraweropenDrawer

使用示例

ts
const { open, close } = useFormDrawer({
  formConfig: {
    schema: supplierSchema,
    fixedColCount: 2,
    grid: { breakpoints: [{ name: 'all', min: 0, cols: 2 }] },
  },
  size: 600,
  submitHandler: async ({ model }) => {
    await api.saveSupplier(model)
    close()
  },
})

await open()
open({ initialValues: { id: '1' }, title: '编辑供应商' })

对外文档:组件 · useFormDrawer


18.3 el-cru-page

定位:CRU(Create / Read / Update)页面的标准封装,将页面头部(el-simple-page-header)与表单(el-form-builder)组合为一个完整的页面级组件。内置保存 / 提交 / 重置通过 portal-vue 落到页头右侧 ${uid}-actions,与 el-form-query / ElSimplePageHeader(无 #title 时)一致。

实现packages/components/cru-page。内置 toolbarActions 生成的 actions[].to 恒为 ${uid}-actions #title 时由本组件在页头 #extra 内挂载 PortalTarget name="${uid}-actions"。依赖 ElFormBuilderactions / submitHandler / saveHandler / resetHandler(见 §11.3 / §11.4)。选项卡下非默认 tab 的内容通过根 Portal 投递至 ${uid}-tab-<value>

CRU = Create + Read + Update,不含 Delete(删除通常在列表页处理)。

Props

ts
interface ElCruPageProps {
  /**
   * 表单实例 uid。
   * 不传则组件内部自动生成(ulid())。
   * 需要从外部操控表单时(如父组件调用 formRef)可手动传入。
   */
  uid?: string

  /** 页面级 loading(例如透传给头部骨架;**不**作为 `el-form-builder` 的 `loading` prop) */
  loading?: boolean

  /** 页面头部配置(透传给 el-simple-page-header) */
  headerConfig: EllSimplePageHeaderProps

  /**
   * 表单配置(透传给 el-form-builder;本组件会注入 **`uid` / `actions` / handlers**)。
   * 自定义扩展按钮与内置按钮共用 **`${uid}-actions`**,以 **`order`** 排序。
   */
  formConfig: Omit<ElFormBuilderProps, 'modelValue' | 'uid' | 'actions' | 'submitHandler' | 'saveHandler' | 'resetHandler'>

  /**
   * 保存回调(不做校验,直接提交)。
   * 对应 el-form-builder actions 中 key='save' 的按钮。
   */
  saveHandler?: (model: Record<string, any>) => void | Promise<void>

  /**
   * 提交回调(validate 通过后执行)。
   * 对应 el-form-builder actions 中 key='submit' 的按钮。
   */
  submitHandler?: (model: Record<string, any>, formRef: FormInstance) => void | Promise<void>
}

Expose

ts
expose({
  uid,       // 当前实例 uid
  formRef,   // el-form-builder expose 的 formRef(el-form 实例)
  model,     // 当前表单数据(响应式)
  getControlRef,
  getFormItemRef,
  formItemRef,
})

内部渲染结构

<el-cru-page>
  ├── <el-simple-page-header v-bind="headerConfig">
  │     · 无 #title:右侧单一 PortalTarget name=${uid}-actions(与 el-form-query 一致)
  │     · 有 #title:cru-page 提供 #extra:header-extra → PortalTarget name=${uid}-actions

  └── <el-form-builder
        :uid="uid"
        v-bind="mergedFormConfig"
        :actions="resolvedActions"
        :submit-handler="submitHandler"
        :save-handler="saveHandler"
      />

toolbarActions 默认 ['save','submit'](不含 reset);resolvedActions 按该数组为每个 key 生成一项,to 恒为 ${uid}-actionsorder10 + i * 10(与 EllActionConfig.order 一致)。自定义 PortalElSimplePageHeader#extra portal(默认 order 5)落在同一 target,靠 order 混排。

Slots

ts
slots: {
  // #title 时:header-extra 在 ${uid}-actions 的 PortalTarget 之前;扩展仍 Portal 到 ${uid}-actions
  'header-extra': ()
  'form-prepend': ()
  'form-append': ()
}

使用示例

vue
<script setup lang="ts">
const cruPageRef = ref()

const schema: EllFormSchema[] = [
  { field: 'name',   label: '供应商名称', controlType: 'input', required: true },
  { field: 'code',   label: '编号',       controlType: 'input' },
  { field: 'type',   label: '类型',       controlType: 'select',
    options: supplierTypes },
  {
    type: 'group',
    field: 'contactGroup',
    label: '联系人信息',
    children: [
      { field: 'contact', label: '联系人', controlType: 'input' },
      { field: 'phone',   label: '电话',   controlType: 'input' },
    ],
  },
]
</script>

<template>
  <el-cru-page
    ref="cruPageRef"
    :header-config="{ title: '供应商入库申请', showBack: true }"
    :form-config="{ schema, colCount: { md: 2, lg: 3 }, initialValues: detail }"
    :save-handler="handleSave"
    :submit-handler="handleSubmit"
  />
</template>

18.4 业务组件目录结构

业务复合组件与基础组件平级,统一在 packages/components/ 下, 由 packages/element-plus-lab 统一发布。 文档示例放在 docs/content/components/business/

packages/components/

├── form-dialog/
│   ├── __tests__/
│   ├── src/
│   │   └── types.ts            ← UseFormDialogOptions / EllFormDialogPayload
│   └── index.ts                ← useFormDialog()

├── form-drawer/
│   ├── __tests__/
│   ├── src/
│   │   └── types.ts            ← UseFormDrawerOptions / EllFormDrawerPayload
│   └── index.ts                ← useFormDrawer()

└── cru-page/
    ├── __tests__/
    ├── src/
    │   ├── cru-page.vue
    │   └── types.ts            ← ElCruPageProps
    └── index.ts                ← el-cru-page

docs/content/components/business/
├── form-dialog.md
├── form-drawer.md
└── cru-page.md

18.5 三者的关系与适用场景

组件适用场景表单位置按钮位置
useFormDialog简单新建/编辑(字段少)弹窗内容区弹窗底部 footer
useFormDrawer复杂新建/编辑(字段多、有分组)抽屉内容区抽屉底部 footer
el-cru-page独立编辑页面(CRU)页面主体页面头部 extra

贡献者

页面历史

Released under the Apache License 2.0.