Vue3动态组件库建设

为什么需要动态组件扩展设计?

Schema 页面可以根据配置渲染搜索区和表格,但"新增商品""修改商品""查看详情"等操作具有共同特点:触发入口在表格中,内容却是不同的业务面板。

如果在页面中直接为每个操作编写抽屉、表单和事件处理函数,会带来以下问题:

  1. SchemaView 必须认识每一种业务操作;商品、订单、客户等模块会不断向其中堆积代码。
  2. 同一字段需要在表格、创建、编辑、详情等区域重复定义。
  3. 新增一个业务面板需要修改页面结构和事件分支,无法做到"增加配置即可接入"。

因此,这里将业务面板设计为可注册、可配置、可由表格事件唤起的动态组件。页面只负责发现和调度组件;组件自身负责展示、请求和提交;字段由 Schema 统一描述。

设计思路

扩展机制由四部分组成:字段配置、组件注册、Schema 派生和事件分发。

scss 复制代码
业务模型 schemaConfig
  │
  ├─ 字段的 createFormOptions / editFormOptions / detailPanelOptions
  ├─ componentConfig:面板级配置(标题、主键、按钮文本)
  └─ tableConfig:操作按钮及目标组件名
       │
       ▼
useSchema 按组件名派生专属 Schema
       │
       ▼
SchemaView 动态渲染已注册组件
       │
       ▼
表格操作事件 → showComponent → 组件 show(rowData)

通过配置描述扩展

商品模型为同一字段定义不同区域的展示方式。例如商品名称同时出现在表格、搜索、创建表单、编辑表单和详情面板中,但每个区域只读取自己的 *Options

css 复制代码
// model/business/model.js
product_name: {
  type: 'string',
  label: '商品名称',
  minLength: 3,
  maxLength: 10,
  tableOptions: { width: 200 },
  searchOptions: {
    comType: 'dynamicSelect',
    api: '/api/proj/product_enum/list'
  },
  createFormOptions: { comType: 'input' },
  editFormOptions: { comType: 'input' },
  detailPanelOptions: {}
}

componentConfig 存放面板级信息,tableConfig 决定由哪个按钮打开哪个面板:

css 复制代码
componentConfig: {
  createForm: { title: '新增商品', saveBtnText: '新增商品' },
  editForm: {
    mainKey: 'product_id',
    title: '修改商品',
    saveBtnText: '修改商品'
  },
  detailPanel: { mainKey: 'product_id', title: '商品详情' }
},
tableConfig: {
  headerButtons: [{
    label: '新增商品',
    eventKey: 'showComponent',
    eventOption: { comName: 'createForm' },
    type: 'primary'
  }],
  rowButtons: [{
    label: '修改',
    eventKey: 'showComponent',
    eventOption: { comName: 'editForm' },
    type: 'warning'
  }]
}

按组件名派生专属 Schema

一个业务 Schema 包含多个区域的配置。 useSchema 使用组件名拼接出选项键,例如传入 createForm 时读取 createFormOptions,从而避免把表格或搜索配置传入表单。

ini 复制代码
// app/pages/dashborad/complex-view/schema-view/hook/schma.js
const buildDtoSchema = (schema, comName) => {
  const dtoSchema = { type: 'object', properties: {} };
  const optionKey = `${comName}Options`;

  for (const key in schema.properties) {
    const props = schema.properties[key];
    if (!props[optionKey]) continue;

    const dtoProps = {};
    for (const pKey in props) {
      if (pKey.indexOf('Option') < 0) dtoProps[pKey] = props[pKey];
    }

    dtoProps.option = props[optionKey];
    if (schema.required?.find((requiredKey) => requiredKey === key)) {
      dtoProps.option.required = true;
    }
    dtoSchema.properties[key] = dtoProps;
  }
  return dtoSchema;
};

每一个组件都会得到一份专属 Schema 配置:

ini 复制代码
const dtoComponents = {};
for (const comName in componentConfig) {
  dtoComponents[comName] = {
    schema: buildDtoSchema(configSchema, comName),
    config: componentConfig[comName]
  };
}
components.value = dtoComponents;

注册和渲染动态组件

组件注册表负责把配置中的字符串名称转换为实际 Vue 组件。新增一个业务面板时,首先需要在这里注册。

css 复制代码
// components/component-config.js
import createForm from './create-form/create-form.vue';
import editForm from './edit-form/edit-form.vue';
import detailPanel from './detail-panel/detail-panel.vue';

export default {
  createForm: { component: createForm },
  editForm: { component: editForm },
  detailPanel: { component: detailPanel }
};

SchemaView 遍历由 useSchema 生成的 components,并通过动态 <component> 挂载它们。组件发出 command 后,页面统一处理表格刷新。

ini 复制代码
<component
  v-for="(item, key) in components"
  :key="key"
  :is="ComponentConfig[key]?.component"
  ref="comListRef"
  @command="onComponentCommand"
/>
ini 复制代码
const onComponentCommand = ({ event }) => {
  if (event === 'loadTableData') {
    tablePanelRef.value.loadTableData();
  }
};

通过表格事件打开目标组件

表格将按钮配置和当前行数据作为 operate 事件参数传出。 SchemaView 通过事件映射表解析事件,而不是在模板里为每个按钮写条件分支。

javascript 复制代码
const EventHandlerMap = { showComponent };

const onTableOperate = ({ btnConfig, rowData }) => {
  const handler = EventHandlerMap[btnConfig.eventKey];
  if (handler) handler({ btnConfig, rowData });
};

function showComponent({ btnConfig, rowData }) {
  const { comName } = btnConfig.eventOption;
  const comRef = comListRef.value.find((item) => item.name === comName);

  if (!comRef || typeof comRef.show !== 'function') {
    console.error(`找不到 com ${comName}`);
    return;
  }
  comRef.show(rowData);
}

为让父组件能够查找和调用子组件,每个动态面板都需要公开同一份最小接口:

ini 复制代码
const name = ref('editForm');

defineExpose({
  name,
  show
});

Schema Form 如何支撑表单类组件

schema-form 只关心字段 Schema,不关心它被创建表单还是编辑表单使用。它由控件注册表选择具体控件,并将校验和取值能力向上暴露。

ini 复制代码
<!-- schema-form.vue -->
<component
  v-for="(itemSchema, key) in schema.properties"
  ref="formComList"
  v-show="itemSchema.option.visible !== false"
  :is="FormItemConfig[itemSchema.option?.comType]?.component"
  :schema-key="key"
  :schema="itemSchema"
  :model="model ? model[key] : undefined"
/>
javascript 复制代码
const validate = () => formComList.value.every((component) => component.validate());

const getValue = () => formComList.value.reduce(
  (value, component) => ({ ...value, ...component.getValue() }),
  {}
);

defineExpose({ validate, getValue });

字段控件使用 AJV 对 Schema 校验。例如数字输入会校验必填、类型和上下界,并通过 getValue() 返回自己的字段值。

ini 复制代码
const validate = () => {
  if (schema.option?.required && !dtoValue.value) {
    validTips.value = '不能为空';
    return false;
  }

  const validateBySchema = ajv.compile(schema);
  if (dtoValue.value && !validateBySchema(dtoValue.value)) {
    validTips.value = '不符合要求';
    return false;
  }
  return true;
};

三个动态组件的具体实现(可扩展更多)

createForm:新增并刷新列表

创建面板把 createForm 专属 Schema 传给 schema-form。提交前调用表单校验,通过后发送 POST 请求;成功后关闭抽屉并通知父页面刷新。

ini 复制代码
// components/create-form/create-form.vue
const save = async () => {
  if (loading.value || !schemaFormRef.value.validate()) return;
  loading.value = true;

  const res = await $curl({
    method: 'post',
    url: api.value,
    data: schemaFormRef.value.getValue()
  });

  loading.value = false;
  if (!res?.success) return;

  ElNotification({ title: '创建成功', message: '创建成功', type: 'success' });
  isShow.value = false;
  emit('command', { event: 'loadTableData' });
};

editForm:加载、回填、保存

编辑面板从行数据读取 mainKey 指定的主键,先请求详情,再将详情数据作为 schema-formmodel。保存时将主键和表单值合并后提交。

ini 复制代码
const show = (rowData) => {
  const { config } = components.value[name.value];
  mainKey.value = config.mainKey;
  mainValue.value = rowData[config.mainKey];
  dtoModel.value = {};
  isShow.value = true;
  fetchFormData();
};

const fetchFormData = async () => {
  const res = await $curl({
    method: 'get',
    url: api.value,
    query: { [mainKey.value]: mainValue.value }
  });
  if (res?.success && res.data) dtoModel.value = res.data;
};

const save = async () => {
  if (!schemaFormRef.value.validate()) return;
  const res = await $curl({
    method: 'put',
    url: api.value,
    data: { [mainKey.value]: mainValue.value, ...schemaFormRef.value.getValue() }
  });
  if (res?.success) emit('command', { event: 'loadTableData' });
};

detailPanel:按 Schema 只读展示

详情面板同样用行数据中的主键查询单条记录,但不渲染可编辑控件,而是遍历详情 Schema 的字段并显示对应数据。

ruby 复制代码
<el-row
  v-for="(item, key) in components[name]?.schema?.properties"
  :key="key"
  class="row-item"
>
  <el-row class="item-label">{{ item.label }}:</el-row>
  <el-row class="item-value">{{ dtoModel[key] }}</el-row>
</el-row>

总结

在schema配置下,SchemaView 不直接处理新增、编辑和详情的业务细节,只负责组件注册、渲染和事件分发。createFormeditFormdetailPanel 分别完成创建、回填修改和只读展示,schema-form 负责字段渲染、校验和取值。由组件发送 loadTableData 事件,再由页面刷新表格,形成了动态面板操作后的页面更新闭环。

相关推荐
码上暴富1 小时前
Cursor / VS Code 自定义文件颜色
前端·vscode
开开心心就好1 小时前
电子教鞭工具支持画框写字插图片功能齐全
android·开发语言·前端·javascript·人工智能·pdf·html
CarIise2 小时前
下拉菜单HTML/CSS/JS实现
前端·css
2601_962071572 小时前
Java进阶(vue基础)
前端·javascript·vue.js
研☆香2 小时前
数组方法 splice讲解 拓展
开发语言·前端·javascript
码视野2 小时前
基于 Spring Boot + Vue3 的【城市地下燃气管网微泄漏感知与相邻地下空间燃爆预警中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
淡海水2 小时前
05-03-栈队列-PriorityQueue-TElement-TPriority-NET6优先队列语义与四叉堆实现
服务器·前端·c#·priorityqueue·clr·telement
ITresearchGuest3 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·javascript·langchain
YHHLAI3 小时前
React + TypeScript 实战:从零构建颜色选择器应用
前端·react.js·typescript