全栈项目实践四:动态组件

32 阅读3分钟

本文内容学习自 哲玄前端 《大前端全栈实践》课程

接第三章所述

{
  // 2.1 当 moduleType == schema 时
  schemaConfig: {
    api: '', // 数据源 API (遵循 RESTFUL 规范)
    schema: {
      // 模块数据结构
      type: 'object',
      properties: {
        prop: {
          ...schema, // 标准 schema 结构
          type: '', // 字段类型
          label: '', // 字段中文名
          // 字段在 table 中的相关配置
          tableOption: {},
          // 字段在 search-bar 中的相关配置
          searchOption: {},

          // 字段在 create-form 中的相关配置
          createFormOption: {
            ...elComponentConfig, // 标准 el-component 配置
            componentType: '', // 配置组件类型, 枚举值: 'input' | 'input-number' | 'select'
            visible: true, // 是否展示, 默认 true(false则不在表格展示)
            disabled: false, // 是否禁用, 默认 false
            default: '', // 默认值

            // componentType === 'select' 时, 可填
            enumList: [{ label: '', value: '' }], // 下拉框选项
          },
          // 字段在 edit-form 中的相关配置
          editFormOption: {
            ...elComponentConfig, // 标准 el-component 配置
            componentType: '', // 配置组件类型, 枚举值: 'input' | 'input-number' | 'select'
            visible: true, // 是否展示, 默认 true(false则不在表格展示)
            disabled: false, // 是否禁用, 默认 false
            default: '', // 默认值

            // componentType === 'select' 时, 可填
            enumList: [{ label: '', value: '' }], // 下拉框选项
          },
          // 字段在 detail-panel 中的相关配置
          detailPanelOption: {
            ...elComponentConfig, // 标准 el-component 配置
          },
        },
      },
    },
    // table 相关配置
    tableConfig: {
      headerButtons: [
        {
          ...elButtonConfig, // 标准 el-button 配置
          label: '', // 按钮名称
          eventKey: '', // 按钮事件名称
          // 按钮具体配置
          eventOption: {
            // eventKey === 'showComponent'
            componentName: '', // 可以填动态组件名称
          },
        },
      ],
      rowButtons: [
        {
          ...elButtonConfig, // 标准 el-button 配置
          label: '', // 按钮名称
          eventKey: '', // 按钮事件名称
          // 按钮具体配置
          eventOption: {
            // eventKey === 'showComponent'
            componentName: '', // 可以填动态组件名称

            // eventKey === 'remove'
            params: {
              // paramKey 为参数的键值,
              // rowValueKey 为参数值(格式为 schema::tableKey 时, 到 table 中找相应字段)
              paramKey: rowValueKey,
            },
          },
        },
      ],
    },
    // search-bar 相关配置
    searchConfig: {},
    // 动态组件相关配置
    componentConfig: {
      // create-form 相关配置
      createForm: {
        title: '', //表单标题
        saveButtonText: '', // 保存按钮文案
      },
      // edit-form 相关配置
      editForm: {
        mainKey: '', // 表单主键, 唯一标识要修改的数据对象
        title: '', //表单标题
        saveButtonText: '', // 保存按钮文案
      },
      // detail-panel 相关配置
      detailPanel: {
        mainKey: '', // 表单主键, 唯一标识要修改的数据对象
        title: '', //表单标题
      },
    },
  },
},

字段在不同动态组件中的相关配置, Option前缀对应 componentConfig 中的键值

  • 例如: componentOption.createFormOption, 字段中的配置就是 createFormOption

components = { componentKey: { schema: {}, config: {} } }

通过在DSL模板中添加 componentConfig,并且在字段中添加对应的 ${componentName}Option 配置(可以在buttons里面的 componentName 配置对应组件进行调用),再通过解析 schema hooks进行配置的具体解析获取到各个 option 和 config。

import { nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'

import { useMenuStore } from '$store/menu'

// 构建 schema 方法, 提取 {optionName}Option
const buildDtoSchema = (_schema, optionName) => {
  if (!_schema.properties) return

  const dtoSchema = {
    type: 'object',
    properties: {},
  }

  // 处理 required 字段
  const required = new Set(_schema.required || [])

  for (const prop in _schema.properties) {
    const schemaProp = _schema.properties[prop]
    if (schemaProp[`${optionName}Option`]) {
      let dtoProp = {}
      for (const key in schemaProp) {
        if (key.endsWith('Option')) continue
        dtoProp[key] = schemaProp[key]
      }
      dtoProp = Object.assign({}, dtoProp, { option: schemaProp[`${optionName}Option`] })

      if (required.has(prop)) dtoProp.option.required = true

      dtoSchema.properties[prop] = dtoProp
    }
  }

  return dtoSchema
}

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 相关配置, 给 schema-view 解析
  const buildData = () => {
    // 先根据 sider_key, 再根据 key
    const { key, sider_key } = route.query

    const menuItem = menuStore.findMenuItem({
      key: 'key',
      value: sider_key ?? key,
    })

    if (menuItem && menuItem.schemaConfig) {
      const { schemaConfig } = menuItem
      const schema = JSON.parse(JSON.stringify(schemaConfig.schema))

      api.value = schemaConfig.api ?? ''

      // 先清空再赋值
      tableSchema.value = {}
      tableConfig.value = undefined
      searchSchema.value = {}
      searchConfig.value = undefined
      components.value = {}
      // nextTick 确保清空操作完成并等待一个 DOM 更新周期后, 再执行新数据的赋值
      nextTick(() => {
        // 构造 tableSchema, tableConfig
        tableSchema.value = buildDtoSchema(schema, 'table')
        tableConfig.value = schemaConfig.tableConfig
        // 构造 searchSchema, searchConfig
        const dtoSearchSchema = buildDtoSchema(schema, 'search')
        for (const prop in dtoSearchSchema.properties) {
          // 如果路由中存在该字段, 则设置默认值
          if (route.query[prop] !== undefined) dtoSearchSchema.properties[prop].option.default = route.query[prop]
        }
        searchSchema.value = dtoSearchSchema
        searchConfig.value = schemaConfig.searchConfig

        // 构造 components = { componentKey: { schema: {}, config: {} } }
        const { componentConfig } = schemaConfig
        const componentKeys = Object.keys(componentConfig)
        if (componentConfig && componentKeys.length > 0) {
          components.value = componentKeys.reduce((dtoComponents, componentKey) => {
            return {
              ...dtoComponents,
              [componentKey]: {
                schema: buildDtoSchema(schema, componentKey),
                config: componentConfig[componentKey],
              },
            }
          }, {})
        }
      })
    }
  }

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

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

config 是针对整个组件的配置, 而 option 是针对字段在组件中的配置(hook 中的 schema),两者需要区分。