Elpis 第四阶段· Vue3 完成动态组件建设

一.前言

我们在第三阶段中学会了如何通过Schema配置Dashboard 模板引擎实现了页面的自动渲染流程,但是缺乏对数据进行操作的功能,所以在本阶段重点就是Schema配置与动态操作组件配合来实现数据的增删改查操作

二.DSL中Schema 动态组件配置

在基于第三章中的Schema配置中增加componentConfig配置,来描述不同的操作加载不同的动态组件

JavaScript 复制代码
componentConfig: {
     // create-from 表单相关配置
      createFrom: {
         title: '',// 表单标题
         saveBtnText: '',// 保存按钮文本
       },
     //edit-from 表单相关配置
      editFrom: {
        mainKey: '', //表单主键,用于唯一标识要修改的数据对象
        title: '',// 表单标题
        saveBtnText: '',// 保存按钮文本
      },
     // detailPanel 详情相关 
       detailPanel: {
         mainKey: '', //表单主键,用于唯一标识要修改的数据对象
         title: '',// 标题
     },
},

动态组件加载触发加载的条件:

  1. tableConfig配置项中的表格头部操作和表格操作栏按钮增加 eventKey(触发事件)和eventOption.comName触发加载的组件名称
  2. 删除操作特殊处理eventOption.params中配置指定key字段进行删除操作
JavaScript 复制代码
tableConfig: {
    headerButtons: [{
      label: '新增商品',
      eventKey: 'showComponent',
      eventOption: {
            comName: 'createForm'
      },
      type: 'primary',
      plain: true
      }],
       rowButtons: [
       {
         label: "查看详情",
         eventKey: 'showComponent',
         type: 'primary',
         eventOption: {
          comName: 'detailPanel'
         },
      },
      {
          label: "修改",
          eventKey: 'showComponent',
          type: 'warning',
          eventOption: {
              comName: 'editForm'
          },
      },
      {
          label: "删除",
          eventKey: 'remove',
          eventOption: {
              params: {
                  product_id: "schema::product_id"
              }
          },
          type: 'danger',
      }],
},

三. 动态组件的渲染机制

1. schema-view 动态组件的引用

schema-view中通过对component-config.js中动态组件引入关系灵活组件管理

JavaScript 复制代码
// 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'

const ComponentConfig = {
    createForm: {
        component: createForm,
    },
    editForm: {
        component: editForm,
    },
    detailPanel: {
        component: detailPanel,
    },

export default ComponentConfig

2.动态组件的表单控件

针对于createFormeditForm动态组件进行的通用的schema-form widgets组件封装,对于schema-form表单实现结合shema中每个字段中配置的comType进行不同的表单控件渲染

同样通过form-item-config.js单独配置来管理表单控件的引入关系,便于后续的自定义表单控件的开发

JavaScript 复制代码
// form-item-config.js
import input from './complex-view/input/input.vue'
import inputNumber from './complex-view/input-number/input-number.vue'
import select from './complex-view/select/select.vue'

const FormItemConfig = {
    input: {
        component: input,
    },
    inputNumber: {
        component: inputNumber,
    },
    select: {
        component: select,
    },
}
export default FormItemConfig

进行表单组件封装时,我们要考虑如何校验表单与获取表单内容,在表单控件组件中我们都会导出name属性(控件名称),validate函数(校验函数),getValue函数(获取值)

JavaScript 复制代码
// 以 input 控件为例子
// 校验函数 ,通过ajv校验当前的字段的shema中配置字段
// 例如input 中以字符串为主, 可能在schema中添加maxLength,minLength进行长度的要求
const validate = () => {
    validTips.value = null
    const { type } = schema
    //校验是否必填
    if (schema.option?.required && !dtoValue.value) {
        validTips.value = '不能为空'
        return false
    }
    //ajv 校验schema
    if (dtoValue.value) {
        const validate = ajv.compile(schema)
        const vaild = validate(dtoValue.value)
        if (!vaild && validate.errors && validate.errors[0]) {
            const { keyword, params } = validate.errors[0]
            if (keyword === 'type') {
                validTips.value = `类型必须为${type}`
            } else if (keyword === 'maxLength') {
                validTips.value = `最大长度应为:${params.limit}`
            } else if (keyword === 'minLength') {
                validTips.value = `最小长度应为:${params.limit}`
            } else if (keyword === 'pattern') {
                validTips.value = `格式不正确`
            } else {
                console.log(validate.errors[0]);
                validTips.value = '不符合要求'
            }
            return false
        }
    }
    return true
}

//schema 配置
product_name: {
  type: 'string',
  label: '商品名称',
  maxLength: 10, //最大长度校验条件
  minLength: 3, //最小长度校验条件
  tableOption: {
      width: 200,
  },
  searchOption: {
      comType: 'dynamicSelect',
      placeholder: '请选择商品名称',
      api: '/api/proj/product_enum/list',
  },
  createFormOption: {
      comType: 'input', //在不同组件中该字段以什么样的表单控件进行展示
  },
  editFormOption: {
      comType: 'input',
      default: '测试一下'
  },
  detailPanelOption: {},
},

四. schema 解析Hook

通过schema配置转化为可视化界面中最重要的解析工具,从schema中将各个不同配置进行抽离出去形成新的option给组件进行页面渲染。

JavaScript 复制代码
import { ref, watch, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useMenuStore } from '$store/menu.js'

export const useSchema = () => {
    const route = useRoute()
    const menuStore = useMenuStore()

    const api = ref('')
    const tableSchema = ref({})
    const tableConfig = ref()
    const searchSchema = ref({})
    const searchConfig = ref()
    const components = ref({})

    //构造 schemaConfig 相关配置,,输送给 shemaView解释
    const buildData = () => {
        const { key, sider_key: siderKey } = route.query

        const mItem = menuStore.findMenuItem({
            key: 'key',
            value: siderKey ?? key
        })
        if (mItem && mItem.schemaConfig) {
            const { schemaConfig: sConfig } = mItem
            const configSchema = JSON.parse(JSON.stringify(sConfig.schema))

            api.value = sConfig.api ?? ''
            tableSchema.value = {}
            tableConfig.value = undefined
            searchSchema.value = {}
            searchConfig.value = undefined
            components.value = {}

            nextTick(() => {
                //构造 tableSchema 和 tableConfig
                tableSchema.value = buildDtoSchema(configSchema, 'table')
                tableConfig.value = sConfig.tableConfig
                //构造 searchSchema 和 searchConfig
                const dtoSearchSchema = buildDtoSchema(configSchema, 'search')
                for (const key in dtoSearchSchema.properties) {
                    if (route.query[key] !== undefined) {
                        dtoSearchSchema.properties[key].option.default = route.query[key]
                    }
                }
                searchSchema.value = dtoSearchSchema
                searchConfig.value = sConfig.tableConfig

                //构造 components = { comKey :{ schema:{} , config:{} } } 
                const { componentConfig } = sConfig
                if (componentConfig && Object.keys(componentConfig).length > 0) {
                    const dtoComponents = {}
                    for (const comName in componentConfig) {
                        dtoComponents[comName] = {
                            schema: buildDtoSchema(configSchema, comName),
                            config: componentConfig[comName]
                        }
                    }
                    components.value = dtoComponents
                }
            })
        }
    }

    //通用构建 schema方法
    const buildDtoSchema = (_schema, comName) => {
        if (!_schema?.properties) { return {} }
        const dtoSchema = {
            type: 'object',
            properties: {}
        }
        // 提取有效 schema 字段量   
        for (const key in _schema.properties) {
            const props = _schema.properties[key]
            if (props[`${comName}Option`]) {
                let dtoProps = {}
                //提取 props 中非 option的部分,存放到 dtoProps中
                for (const pKey in props) {
                    if (pKey.indexOf('Option') < 0) {
                        dtoProps[pKey] = props[pKey]
                    }
                }
                //处理 comName Option
                dtoProps = Object.assign({}, dtoProps, { option: props[`${comName}Option`] })
                //处理 required 字段
                const { required } = _schema
                if (required && required.find(pk => pk === key)) {
                    dtoProps.option.required = true
                }
                dtoSchema.properties[key] = dtoProps
            }
        }

        return dtoSchema
    }
    watch([
        () => route.params.key,
        () => route.params.sider_key,
        () => menuStore.menuList,
    ], () => {
        buildData()
    }, { deep: true })

    onMounted(() => {
        buildData()
    })

    return {
        api,
        tableSchema,
        tableConfig,
        searchSchema,
        searchConfig,
        components,
    }
}

五. 总结

  1. 在动态组件封装中,使用配置文件统一组件引入管理,保持封装的灵活和扩展性

  2. 完善schema配置,以componentConfig加载不同组件,增加不同组件的xxxxxOption配置完成该组件下不同的控件显示,最终形成我们所需要的交互动作

相关推荐
akira09122 小时前
滚动控制视频播放是如何实现的?GSAP ScrollTrigger + seek 实践 vivo官网案例
前端·产品
用户636836608552 小时前
前端使用nuxt.js的seo优化
前端
OldBirds2 小时前
烧脑时刻:Dart 中异步生成器与流
前端·后端
湛海不过深蓝2 小时前
【echarts】折线图颜色分段设置不同颜色
前端·javascript·echarts
昨晚我输给了一辆AE862 小时前
关于 react-hook-form 的 isValid 在有些场景下的值总是 false 问题
前端·react.js
xinyu_Jina2 小时前
Calculator Game:WebAssembly在计算密集型组合优化中的性能优势
前端·ui·性能优化
JustHappy2 小时前
「2025年终个人总结」🤬🤬回答我!你个菜鸟程序员这一年发生了啥?
前端
BD_Marathon2 小时前
Vue3_计算属性
javascript·vue.js·ecmascript
啃火龙果的兔子2 小时前
可以指定端口启动本地前端的npm包
前端·npm·node.js