复合表单校验(层级比较深时点击提交触发子表校验)

230 阅读2分钟

1、需求描述:

低代码复合表单提交时需触发子表校验,校验通过才能提交,否则不允许提交。

2、难点:

组件层级: TabInline.vue(内层)=>SubTable.vue=>GenerateNest.vue=>GenerateForm.vue(校验层)=>ViewRender.vue(外层) 触发按钮在外层,子表校验触发函数在内层 点击外层的提交按钮时不仅需要触发表单本身的校验还需触发子表校验,这时你会发现跨了很多层级,使用emit不再是一个合适的处理方式(一层一层传递太过繁琐,并且容易出问题)

3.处理方式:

将跨组件调用的所需控件内的验证函数存储起来并暴露出去,在需要使用的地方动态引入 注意事项:同时需要定义一个清空函数,在校验层初次渲染(onMounted)时需先清除下存储的验证函数

4、代码(代码比较多,这边只贴出主要代码)

1、存储需要跨组件调用的所需控件内的验证函数
/**
 * 存储需要跨组件调用的所需控件内的验证函数
 */
export const formCtrlValidatorFun = []
/**
 * 清空formCtrlValidatorFun
 */
export function clearFormCtrlValidatoFun() {
  formCtrlValidatorFun.splice(0, formCtrlValidatorFun.length)
}

2、内层组件(TableInline)将校验函数存储起来
<script setup>
  import { inject } from 'vue'
  // 引入需暴露出去的函数数组
  import { formCtrlValidatorFun } from '@/components/FormEngine/core/useFunCallStack'
  ...//省略部分代码
  // 表格行内校验
  async function validateTableInline() {
    // 新增、修改时进行字段校验
    const valid = await formRef.value.validate()
    if (valid) {
      Message.success('操作成功!')
      return true
    } else {
      Message.error('校验失败!')
      return false
    }
  }
  // 保存
  async function handleSave(index) {
    // 逐个遍历并进行赋值
    fieldColumns.value.forEach(item => {
      const { model } = item
      if(listData.value.length > 0){
        listData.value[index][model] = editObj.value[model]
      }
    })
    const valid = await validateTableInline()
    if (valid) {
      editIndex.value = -1
      // editIsCreate.value = false
      // editIsEdit.value = false
      disabled.value = false
    }
    return valid
  }
  // 引入表单校验层provide的是否是编辑模式的状态值(存在预览和编辑状态)
  const isEditMode = inject('isEditMode')
  if (!isEditMode) {
  // 供外部调用的特殊函数(完成保存和校验)
  formCtrlValidatorFun.push(async () => {
    let res = true
    if (editIndex.value > -1) {
      res = await handleSave(editIndex.value)
    }
    return res
  })
}
</script>
3、点击提交触发表单校验层的getFormData函数(包含表单校验功能)
<script setup>
    // 统一事件处理,操作事件传参需要根据不同事件类型分别传入
    async function handleEvent(event) {
      if (event) {
        // console.log('---- action event trigger ----', event)
        if (event.key === 'submit') {
          try {
            // 调用表单校验层的函数
            const res = await generateFormRef.value.getFormData()
            // console.log('-----------submit-data----------------')
            const data = buildData(res)
            Store.onTriggerEvent(event, data, viewInfo.value)
          } catch (error) {
            throwError('ViewRender/handleEvent', error)
          }
        } else {
          Store.onTriggerEvent(event)
        }
      }
    }
</script>
4、表单校验层使用暴露出来的函数
<script setup>
  import {onUnmounted, provide } from 'vue'
  // 引入暴露出去的函数数组及清除函数
  import { formCtrlValidatorFun, clearFormCtrlValidatoFun } from '../useFunCallStack'
  onUnmounted(() => {
    clearFormCtrlValidatoFun()
  })

  ...//省略部分代码
  // 获取数据对象
  function getFormData() {
    return new Promise((resolve, reject) => {
      generateFormRef.value.validate(async valid => {
        if (valid) {
          // 重点代码
          for (const fun of formCtrlValidatorFun) {
            const res = await fun()
            if (!res) {
              reject(new Error('校验失败'))
              return
            }
          }
          resolve(models.value)
        } else {
          reject(new Error('校验失败'))
        }
      })
    })
  }
</script>

5、实现效果

image.png

附上相关组件原码(因为组件太多,这边只贴出相关组件代码): TableInline.vue

<template>
  <div class="sublist-table p16" v-if="render && columns.length > 0">
    <div class="mb-8" v-if="!isDetailView">
      <slot>
        <b-button icon="plus" type="primary" :disabled="disabled" @click="handleAdd">新增</b-button>
      </slot>
    </div>
    <b-form ref="formRef" :model="editObj" :rules="rules">
      <b-table :columns="columns" :data="listData" size="small" border>
        <template v-for="slot in fieldSlots" :key="slot" v-slot:[slot]="{ index, row }">
          <b-form-item
            :prop="slot"
            :class="editIndex === index && editIsEdit ? 'has-margin' : 'no-margin'"
          >
            <EventCtrl
              v-if="editIndex === index"
              :cfg="cfg"
              :model="slot"
              v-model:context="ctx"
              v-model:list="listData"
              v-model:row="editObj"
            />
            <span v-else>{{ echoCorrectly(row, slot) }}</span>
          </b-form-item>
        </template>
        <!-- 操作列 -->
        <template #action="{ index, row }">
          <div v-if="editIndex === index">
            <template v-if="editIsCreate">
              <b-button size="mini" type="success" transparent @click="handleSave(index)">
                新增
              </b-button>
              <b-button type="danger" size="mini" transparent @click="handleRemove(index, row)">
                删除
              </b-button>
            </template>
            <template v-else>
              <b-button size="mini" type="success" transparent @click="handleSave(index)">
                保存
              </b-button>
              <b-button size="mini" type="primary" transparent @click="handleCancel(index)">
                取消
              </b-button>
            </template>
          </div>
          <div v-else>
            <b-button
              :type="editIsCreate ? 'default' : 'primary'"
              size="mini"
              :transparent="!editIsCreate"
              :disabled="disabled"
              @click="handleEdit(row, index)"
            >
              修改
            </b-button>
            <b-button
              :type="editIsCreate ? 'default' : 'danger'"
              size="mini"
              :transparent="!editIsCreate"
              :disabled="disabled"
              @click="handleRemove(index, row)"
            >
              删除
            </b-button>
          </div>
        </template>
      </b-table>
    </b-form>
    <div class="trigger-mask" v-if="!eventTrigger" />
  </div>
  <!--   <b-ace-editor :model-value="JSON.stringify({ forms, editObj, rules }, null, 2)" />-->
</template>

<script setup>
import { EmitsBus, emitKey } from '@/components/FormEngine/core/emits-bus'
import { computed, ref, watch, nextTick, inject } from 'vue'
import { formCtrlValidatorFun } from '@/components/FormEngine/core/useFunCallStack'
import EventCtrl from './EventCtrl.vue'
import { buildRules } from '@/components/FormEngine/util/validator'
import { Message } from 'bin-ui-next'

const emit = defineEmits(['update:modelValue', 'update:context'])
const props = defineProps({
  cfg: {
    type: Object,
    default: () => ({}),
  },
  modelValue: {
    type: Array,
    default: () => [],
  },
  context: {
    type: Object,
    default: () => ({}),
  },
  eventTrigger: {
    type: Boolean,
    default: false,
  },
  isDetailView: {
    type: Boolean,
    default: false,
  },
})
const isEditMode = inject('isEditMode')
const fieldColumns = computed(() => props.cfg.columnsConfig)
// 列表里需要显示的字段
const colFields = computed(() => fieldColumns.value.filter(i => i.showList))

// 列表里需要的插槽
const fieldSlots = computed(() => colFields.value.map(i => i.model))

// 列表里的校验集合
const allRules = computed(() =>
  fieldColumns.value.map(i => ({
    rules: i.rules,
    slot: i.model || i.fieldName,
  })),
)
const listData = computed({
  get: () => props.modelValue,
  set: val => emit('update:modelValue', val),
})

const ctx = computed({
  get: () => props.context,
  set: val => emit('update:context', val),
})

const disabled = computed({
  get: () => editIsCreate.value || editIsEdit.value,
  set: val => {
    editIsCreate.value = val
    editIsEdit.value = val
  },
})
// 取得对应模式的配置项关系
// const tableEditModel = computed(() => props.cfg.command.editModel)
// const tableEditConfig = computed(() => props.cfg.command[tableEditModel.value])

const render = ref(false)
const columns = ref([])
const editIndex = ref(-1)
const editObj = ref({})
const editIsCreate = ref(false) // 由新增按钮触发,处于新增的编辑状态
const editIsEdit = ref(false) // 由编辑按钮触发,处于编辑状态
const copyEditObj = ref({}) // 备份的行数据

const formRef = ref(null)
const rules = ref({})

function buildColumns() {
  columns.value = colFields.value.map(i => ({
    title: i.name || i.fieldDesc,
    slot: i.model || i.fieldName,
    ...i.columnCfg,
  }))

  if (props.isDetailView) return
  columns.value.push({
    title: '操作',
    slot: 'action',
    align: 'center',
    fixed: 'right',
    width: fieldColumns.value.length ? 180 : null,
  })
}

function buildRulesObj() {
  allRules.value.forEach(item => {
    let rule = buildRules(item.rules).filter(item => item !== null)
    if (rule.length > 0) {
      rules.value[item.slot] = rule
    }
  })
  return rules.value
}

watch(
  () => props.cfg.columnsConfig,
  () => {
    render.value = false
    buildColumns()
    buildRulesObj()
    nextTick(() => {
      render.value = true
    })
  },
  { immediate: true, deep: true },
)

function handleAdd() {
  const row = {}
  fieldColumns.value.forEach(item => {
    row[item.model] = item.options.defaultValue ? item.options.defaultValue : null // 默认值设置
  })
  listData.value.push(row)
  handleEdit(row, listData.value.length - 1)
  editIsCreate.value = true
}

function handleEdit(row, index) {
  editObj.value = { ...row }
  copyEditObj.value = { ...row }
  editIndex.value = index
  editIsEdit.value = true
}

function handleRemove(index, row) {
  listData.value.splice(index, 1)
  if (row && row.id) {
    EmitsBus.emit(emitKey.delSubItem, {
      formId: props.cfg.relationConfig.relateFormId,
      delId: row.id,
    })
  }
  // editIsCreate.value = false
  // editIsEdit.value = false
  disabled.value = false
}

// 保存
async function handleSave(index) {
  // 逐个遍历并进行赋值
  fieldColumns.value.forEach(item => {
    const { model } = item
    listData.value[index][model] = editObj.value[model]
  })
  const valid = await validateTableInline()
  if (valid) {
    editIndex.value = -1
    // editIsCreate.value = false
    // editIsEdit.value = false
    disabled.value = false
  }
  return valid
}

// 取消
function handleCancel(index) {
  // 清除可能存在的校验结果
  formRef.value.clearValidate()
  // 取消时恢复为修改之前的值
  listData.value[index] = copyEditObj.value
  editIndex.value = -1
  // editIsCreate.value = false
  // editIsEdit.value = false
  disabled.value = false
}

// 表格行内校验
async function validateTableInline() {
  // 新增、修改时进行字段校验
  const valid = await formRef.value.validate()
  if (valid) {
    Message.success('操作成功!')
    return true
  } else {
    Message.error('校验失败!')
    return false
  }
}

function echoCorrectly(row, slot) {
  for (let i = 0; i < colFields.value.length; i++) {
    if (colFields.value[i].fieldName === slot) {
      if (
        colFields.value[i].type === 'radio' ||
        colFields.value[i].type === 'select' ||
        colFields.value[i].type === 'checkbox'
      ) {
        if (colFields.value[i].options.options && colFields.value[i].options.options.length > 0) {
          const item = colFields.value[i].options.options.filter(item =>
            row[slot].split(',').includes(item.key),
          )
          return item.map(item => item.value).join(',')
        }
      }
    }
  }
  return row[slot]
}

if (!isEditMode) {
  // 供外部调用的特殊函数(完成保存和校验)
  formCtrlValidatorFun.push(async () => {
    let res = true
    if (editIndex.value > -1) {
      res = await handleSave(editIndex.value)
    }
    return res
  })
}
</script>
<style lang="stylus" scoped>
.sublist-table {
  position: relative;

  .has-margin{
    margin 20px 0;
  }

  .no-margin{
    margin 0;
  }

  .trigger-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
</style>

SubTable.vue

<template>
  <div class="sub-table">
    <b-collapse-wrap>
      <template #title>
        <title-bar :title-style="titleStyle" tip-pos="left" no-border>
          <b-icon v-if="cfg.title.icon" :name="cfg.title.icon" />
          {{ cfg.title.text || cfg.relationConfig.relateTableDesc || '没有配置表描述' }}
        </title-bar>
      </template>

      <TableInline
        v-if="tableEditModel === 'inline'"
        :cfg="cfg"
        :event-trigger="eventTrigger"
        :isDetailView="isDetailView"
        v-model="listData"
        v-model:context="ctx"
      />

      <TableForm
        v-if="tableEditModel === 'form'"
        :cfg="cfg"
        :event-trigger="eventTrigger"
        :isDetailView="isDetailView"
        v-model="listData"
        v-model:context="ctx"
      />

      <template v-if="tableEditModel === 'select'">
        <TableInline
          v-if="editMode === 'inline'"
          :cfg="cfg"
          :event-trigger="eventTrigger"
          :isDetailView="isDetailView"
          v-model="listData"
          v-model:context="ctx"
        >
          <SelectAddBtn :cfg="cfg" @selected="handleSelected" />
        </TableInline>

        <TableForm
          v-if="editMode === 'form'"
          :cfg="cfg"
          :event-trigger="eventTrigger"
          :isDetailView="isDetailView"
          v-model="listData"
          v-model:context="ctx"
        >
          <SelectAddBtn :cfg="cfg" @selected="handleSelected" />
        </TableForm>
      </template>
    </b-collapse-wrap>
  </div>
</template>

<script>
export default {
  name: 'SubTable',
}
</script>

<script setup>
import TitleBar from '@/components/Common/TitleBar/index.vue'
import TableInline from './TableInline.vue'
import TableForm from './TableForm.vue'
import SelectAddBtn from './SelectAddBtn.vue'
import { computed, watch } from 'vue'
import { Message } from 'bin-ui-next'
import { buildFun } from '@/components/Common/FunBodyEditorHelpInfo/customFunUtil'

const emit = defineEmits(['update:modelValue', 'update:context'])

const props = defineProps({
  cfg: {
    type: Object,
    default: () => ({}),
  },
  modelValue: {
    type: Array,
    default: () => [],
  },
  context: {
    type: Object,
    default: () => ({}),
  },
  eventTrigger: {
    type: Boolean,
    default: false,
  },
  isDetailView: {
    type: Boolean,
    default: false,
  },
})

const titleStyle = computed(() => ({
  padding: '0 12px',
  lineHeight: '40px',
}))
const tableEditModel = computed(() => props.cfg.command.editModel)
const editMode = computed(() => props.cfg.command.select?.editMode)
const fieldColumns = computed(() => props.cfg.columnsConfig)

const listData = computed({
  get: () => props.modelValue,
  set: val => emit('update:modelValue', val),
})

const ctx = computed({
  get: () => props.context,
  set: val => emit('update:context', val),
})

watch(
  listData,
  newVal => {
    if (props.cfg.funcEnable) {
      try {
        const fun = buildFun(props.cfg.funcBody, ...['formModel', 'list'])
        fun(ctx.value, newVal)
      } catch (error) {
        console.error(error)
        Message.warning('表格控件观察函数体异常,请检查相关配置配置。')
      }
    }
  },
  {
    deep: true,
  },
)

function handleSelected(arr) {
  const list = buildValByValueMapping(arr, props.cfg.command.select.valueMapping)
  listData.value.push(...list)
}

function buildValByValueMapping(arr, valueMapping) {
  const list = []
  arr.forEach(item => {
    const obj = {}
    // 构建行对象结构
    fieldColumns.value.forEach(field => {
      obj[field.model] = null
    })
    // 构建映射数据
    valueMapping.forEach(map => {
      obj[map.target] = item[map.source]
    })
    list.push(obj)
  })
  return list
}
</script>

<style lang="stylus" scoped>
.sub-table {
  position: relative;
  margin-bottom: 17px;
  .trigger-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}
</style>

GenerateNest.vue

<template>
  <div v-for="element in list" :key="element.key" class="generate-item">
    <!-- grid 栅格布局 -->
    <b-row
      v-if="element.type === 'grid'"
      type="flex"
      :gutter="element.options.gutter"
      :justify="element.options.justify"
      :align="element.options.align"
    >
      <b-col
        v-for="(col, colIndex) in element.columns"
        :key="colIndex"
        :span="col.span ? col.span : 0"
      >
        <GenerateNest
          :list="col.list"
          :form-config="formConfig"
          v-model="models"
          :is-detail-view="isDetailView"
          :view-id="viewId"
          :form-id="formId"
          :preview="preview"
        />
      </b-col>
    </b-row>

    <!-- 分割线显示内容 -->
    <b-divider
      v-if="element.type === 'divider'"
      :align="element.options.align"
      :dashed="element.options.dashed"
      :style="{ margin: element.options.margin, width: 'auto' }"
    >
      {{ element.name }}
    </b-divider>

    <!-- 表格控件 -->
    <SubTable
      v-if="element.type === 'sub-table'"
      :cfg="element.options"
      :is-detail-view="isDetailView"
      eventTrigger
      v-model="models[element.model]"
      v-model:context="models"
    />

    <!--表格只读控件 -->
    <readlist-control v-if="element.type === 'readlist'" :cfg="element.options" eventTrigger />

    <!--上传控件-->
    <b-form-item v-if="element.type === 'upload-control'" prop="fileList">
      <span :class="{ required: !isDetailView && element.options.required }" />
      <attachment-upload
        v-model="models.fileList"
        :is-detail-view="isDetailView"
        :fileName="element.model"
        :fileList="models.fileList"
        :cfg="element.options"
        :view-id="viewId"
        :form-id="formId"
        :preview="preview"
        @delete-file="deleteFileList(element.model)"
        @add-file="addFileList"
      ></attachment-upload>
    </b-form-item>

    <!-- 分组容器 -->
    <group-container v-if="element.type === 'group-container'" :cfg="element.options">
      <GenerateNest
        :list="element.components"
        :form-config="formConfig"
        v-model="models"
        :is-detail-view="isDetailView"
        :view-id="viewId"
        :form-id="formId"
        :preview="preview"
      />
    </group-container>

    <!-- Tab容器 -->
    <tab-container
      v-if="element.type === 'tab-container'"
      :cfg="element.options"
      :tabs="element.tabs"
    >
      <template v-slot="{ components }">
        <GenerateNest
          :list="components"
          :form-config="formConfig"
          v-model="models"
          :is-detail-view="isDetailView"
          :view-id="viewId"
          :form-id="formId"
          :preview="preview"
        />
      </template>
    </tab-container>

    <!-- 调试字段和配置项 -->
    <!-- <div v-if="!isLayoutWidget(element)" style="border: 1px dashed #333">
      <div>{{ element }}</div>
      <div style="color: red">{{ models }}</div>
    </div> -->
    <generate-form-item
      v-if="!isLayoutWidget(element)"
      v-model="models"
      :element="element"
      :form-config="formConfig"
      :is-detail-view="isDetailView"
      :view-id="viewId"
      :form-id="formId"
    ></generate-form-item>
  </div>
</template>

<script>
import GenerateFormItem from './GenerateFormItem.vue'
import { computed } from 'vue '

// 备注:此处的数组,需要和form-maker中的对应,由于是运行时依赖,因此此部分的内容应该是独立维护
const layoutTypes = [
  'grid',
  'divider',
  'sub-table',
  'readlist',
  'group-container',
  'tab-container',
  'upload-control',
]

export default {
  name: 'GenerateNest',
  components: { GenerateFormItem },
  props: {
    list: {
      type: Array,
      default: () => [],
    },
    modelValue: {
      type: Object,
      default: () => ({}),
    },
    formConfig: {
      type: Object,
      default: () => ({}),
    },
    isDetailView: {
      type: Boolean,
      default: false,
    },
    viewId: {
      type: String,
      default: '',
    },
    formId: {
      type: String,
      default: '',
    },
    preview: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const models = computed({
      get: () => props.modelValue,
      set: val => emit('update:modelValue', val),
    })

    // 判断当前的部件是不是layout布局控件
    function isLayoutWidget(widget) {
      return widget.key && layoutTypes.includes(widget.type)
    }

    function deleteFileList(key) {
      delete models.value.fileList[key]
    }

    function addFileList(data) {
      models.value.fileList = { ...models.value.fileList, ...data }
    }

    return { isLayoutWidget, models, deleteFileList, addFileList }
  },
}
</script>
<style lang="stylus" scope>
.required:before {
  content: '*';
  display: inline-block;
  margin-right: 4px;
  line-height: var(--bin-base-line-height);
  font-family: SimSun;
  font-size: var(--bin-base-font-size);
  color: var(--bin-color-danger);
}
</style>

GenerateForm.vue

<template>
  <div class="fm-style">
    <div flex>
      <div flex-box="1">
        <b-form
          ref="generateFormRef"
          :model="models"
          :rules="rules"
          :label-position="data.config.labelPosition"
          :label-width="`${data.config.labelWidth}px`"
          :label-suffix="data.config.labelSuffix"
          :size="data.config.size"
        >
          <generate-nest
            v-model="models"
            :list="data.list"
            :form-config="data.config"
            :is-detail-view="isDetailView"
            :view-id="viewId"
            :form-id="formId"
            :preview="preview"
          />
        </b-form>
      </div>
      <!-- <div v-if="IS_DEV" style="width: 350px; padding-left: 16px; flex-shrink: 0">
        <b-ace-editor :model-value="JSON.stringify({ models, rules }, null, 2)" readonly />
      </div> -->
    </div>
  </div>
</template>

<script>
import { ref, watch, onUnmounted, provide } from 'vue'
import { useInsEventBusRuntime } from '../InsEvent/useInsEventBusRuntime'
import { useInsEventWatchResForm } from '../InsEvent/useInsEventWatchResForm'
import { IS_DEV } from '@/utils/env'
import { deepCopy } from '@/utils/util'
import { formCtrlValidatorFun, clearFormCtrlValidatoFun } from '../useFunCallStack'
import { getFieldsCfgByList, buildRules } from '@/components/FormEngine/util/validator'
import {
  useFormViewLoadedAction,
  useFormViewClosedAction,
} from '../../runtime/use-form-base-action'
import GenerateNest from './GenerateNest.vue'

const READ_LIST = 'readlist'
const SUB_TABLE = 'sub-table'

export default {
  name: 'GenerateForm',
  components: { GenerateNest },
  props: {
    data: {
      type: Object,
      default: () => ({}),
    },
    defaultModel: {
      type: Object,
      default: () => ({}),
    },
    formTreeCfg: {
      type: Object,
    },
    isDetailView: {
      type: Boolean,
      default: false,
    },
    viewId: {
      type: String,
      default: '',
    },
    formId: {
      type: String,
      default: '',
    },
    preview: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['update:uploadRequired'],
  setup(props, { emit }) {
    provide('isEditMode', false)
    onUnmounted(() => {
      useFormViewClosedAction(props.data.config.viewCloseActionList)
      clearFormCtrlValidatoFun()
    })

    const models = ref({})
    const rules = ref({})
    const generateFormRef = ref(null)

    // 复合表配置list
    const subTableListCopy = ref([])
    //有子表的订单明细页面
    const subListCopy = ref(null)
    //有必填校验的上传附件的唯一标识组成的数组
    const uploadRequiredList = ref([])

    // 创建指令事件总线
    useInsEventBusRuntime()

    // 动态拼接model
    function generateModel() {
      // 是否有默认对象用作数据回显
      const normalObj = props.defaultModel
      if (normalObj && normalObj.id) {
        models.value.id = normalObj.id
      }
      let { fieldList, subList, subTableList } = getFieldsCfgByList(props.data.list)

      subTableListCopy.value = deepCopy(subTableList)
      subListCopy.value = deepCopy(subList)
      // 实体字段进行初始化属性
      fieldList.forEach(item => {
        let value = item.options.defaultValue
        // 判断是否有默认对象,如果有表示是修改时的回显数据填充
        if (normalObj && normalObj[item.model]) {
          value = normalObj[item.model]
        }
        // 如果是列表选择字段,则初始化时需要根据回显字段扩充实际对象
        if (item.type === 'list-select') {
          const { fillKey } = item.options
          if (fillKey) {
            models.value[fillKey] = normalObj[fillKey] ?? '' // 扩充回显字段
          }
        } else if (item.type === 'upload-control') {
          models.value.fileList = normalObj.fileList
        } else {
          models.value[item.model.toLowerCase()] = value
        }

        // 判断是否是详情模式,如果不是详情则再去初始化校验数据
        if (!props.isDetailView && item.type !== READ_LIST && item.type !== SUB_TABLE) {
          if (item.rules) {
            let rule = buildRules(item.rules, models.value, props.viewId, { ...models.value })
            if (rule.length > 0) {
              if (item.type === 'upload-control') {
                rules.value.fileList = rule
                uploadRequiredList.value.push(item.model)
              } else {
                rules.value[item.model] = rule
              }
            }
          }
        }
      })
      emit('update:uploadRequired', uploadRequiredList.value)
      // 子标数据回显初始化
      subList.forEach(item => {
        models.value[item.model.toLowerCase()] =
          normalObj && normalObj[item.model] ? normalObj[item.model] : []
      })

      // 复合表内容生成
      subTableList.forEach(item => {
        console.log(item.model)
        models.value[item.model] = normalObj?.[item.model] ?? []
      })

      return {
        fieldList,
      }
    }

    // 处理树是否需要处理树结构的内容,如果有则formTreeCfg中会存在config
    function disposeTreeDataToModel() {
      if (!props.formTreeCfg) return
      // 判定当前全局设置里,是否有配置树结构,如果有则需要扩展填充字段和所带入的数据
      const { config, currentTreeNode } = props.formTreeCfg
      // 如果配置项存在且当前树节点值存在,则为新增带入
      if (config && currentTreeNode) {
        const { nodeIdField: nodeKey, filterListField: modelKey, formId } = config.fetchParams
        if (nodeKey && modelKey) {
          const value = currentTreeNode[nodeKey]
          //新增节点的时候,如果父节点有值,获取父节点的值
          //父节点没有值 判断当前页的数据是否属于同一张表,是同一张表,就创建根节点  父节点为0
          const isSameTable = props.formId === formId
          models.value[modelKey] = value ? value : isSameTable ? '0' : ''
          models.value[modelKey + '_Echo'] = currentTreeNode.text
            ? currentTreeNode.text
            : isSameTable
            ? '根节点'
            : ''
          console.log(`树节点取值nodekey[${nodeKey}]:${value},填充model[${modelKey}]`)
        }
      }
    }

    // 获取数据对象
    function getFormData() {
      return new Promise((resolve, reject) => {
        generateFormRef.value.validate(async valid => {
          if (valid) {
            for (const fun of formCtrlValidatorFun) {
              const res = await fun()
              if (!res) {
                reject(new Error('校验失败'))
                return
              }
            }
            resolve(models.value)
          } else {
            reject(new Error('校验失败'))
          }
        })
      })
    }

    // 重置form
    function resetForm() {
      //重置子表里面的数据
      if (subListCopy.value && subListCopy.value.length > 0) {
        let modelList = subListCopy.value.map(item => item.model.toLowerCase())
        modelList.forEach(item => (models.value[item] = []))
      }
      //重置子表里面的数据
      if (subTableListCopy.value && subTableListCopy.value.length > 0) {
        let modelList = subTableListCopy.value.map(item => item.model.toLowerCase())
        modelList.forEach(item => (models.value[item] = []))
      }
      generateFormRef.value && generateFormRef.value.resetFields()
    }

    // 初始化form
    function init() {
      models.value = {}
      rules.value = {}
      uploadRequiredList.value = []
      const { fieldList } = generateModel()
      disposeTreeDataToModel()
      console.log('初始化models,rules:', models.value, rules.value)
      useFormViewLoadedAction(props.data.config.initActionList, models, fieldList)
      // 构建监听事件
      useInsEventWatchResForm(props.data.config.watchResActionList, models.value, props.data.list)
    }

    watch(
      () => props.defaultModel,
      () => init(),
      { immediate: true },
    )
    return {
      IS_DEV,
      models,
      rules,
      generateFormRef,
      getFormData,
      resetForm,
    }
  },
}
</script>

ViewRender.vue

<template>
  <b-modal
    width="1500px"
    append-to-body
    destroy-on-close
    v-model="visible"
    :title="topTitle"
    :custom-class="fullscreen ? 'view-modal' : 'view-modal-not-full'"
    :fullscreen="fullscreen"
    :lock-scroll="lockScroll"
    :mask-closable="false"
  >
    <template #ctrl>
      <i
        :class="`b-iconfont b-icon-fullscreen${fullscreen ? '-exit' : ''}`"
        @click="fullscreen = !fullscreen"
      ></i>
    </template>
    <div class="view-box">
      <b-skeleton :loading="!render" animation :rows="8">
        <generate-form
          v-if="render"
          ref="generateFormRef"
          v-model:uploadRequired="uploadRequired"
          :data="widgetForm"
          :default-model="cacheData"
          :form-tree-cfg="formTreeCfg"
          :form-id="viewInfo.formId"
          :view-id="viewInfo.id"
          :is-detail-view="isDetailView"
        />
      </b-skeleton>
    </div>
    <template #footer>
      <div class="t-center" v-if="render">
        <EventButton
          v-for="(config, index) in eventsArr"
          :key="index"
          :config="config"
          :loading="loading"
          @click="handleEvent(config.event)"
        />
      </div>
    </template>
  </b-modal>
</template>

<script>
import { reactive, toRefs, ref, provide, inject, computed } from 'vue'
import { VIEW_TYPE, getViewConfigDetailByKey } from '@/api/modules/form-view.api'
import { getSubDetail, getCommonDetail } from '@/api/modules/form-common.api'
import { throwError, deepCopy, isEmpty } from '@/utils/util'
import GenerateForm from '@/components/FormEngine/core/FormRender/GenerateForm.vue'
import EventButton from '@/components/FormEngine/core/EventSystem/EventButton.vue'
import { getFieldsCfgByList } from '@/components/FormEngine/util/validator'
import { EmitsBus, emitKey } from '@/components/FormEngine/core/emits-bus'
import { Message } from 'bin-ui-next'

export default {
  components: { GenerateForm, EventButton },
  props: {
    lockScroll: {
      type: Boolean,
      default: true,
    },
  },
  setup() {
    const Store = inject('StoreCenter', {})
    const loading = computed(() => Store.state.viewLoading)
    const currentTreeNode = computed(() => Store.state.currentTreeNode)
    const treeConfig = computed(() => Store.treeConfig.value)

    // 实际表单内容需要传入的树结构,节点,配置项信息,如果是修改,则不需要传入节点
    const formTreeCfg = ref(null)
    // 有必填校验的上传附件的唯一标识组成的数组
    const uploadRequired = ref([])
    const generateFormRef = ref(null)
    const state = reactive({
      render: false, // 可以渲染表单的标识
      visible: false,
      fullscreen: true,
    })

    // 视图信息对象
    const viewInfo = ref({})
    const fieldMaps = ref([])
    // 控件form对象json
    const widgetForm = ref({
      list: [],
      config: {},
    })
    // 复合表单关联对象
    const relationConfList = ref(null)

    const fieldCfg = computed(() => getFieldsCfgByList(widgetForm.value.list))

    // 缓存数据,用于修改时回填数据
    const cacheData = ref({})

    // 缓存删除子表id列表
    const cacheSubDelList = ref({}) // 备注,formId为key,值为delList:[id1,id2]

    const topTitle = computed(() => VIEW_TYPE.MapShow[viewInfo.value.viewType])
    // 是否是详情界面
    const isDetailView = computed(() => viewInfo.value.viewType === 'DETAIL')
    // 动态设置按钮配置信息(区分详情和新增修改)
    const eventsArr = computed(
      () => isDetailView.value
        ? widgetForm.value.config.events.filter(item => item?.event.key === 'cancel')
        : widgetForm.value.config.events)

    function _resetState() {
      state.visible = false
      state.render = false
      state.fullscreen = true
      cacheData.value = {}
      cacheSubDelList.value = {}
      widgetForm.value = {}
      viewInfo.value = {}
      fieldMaps.value = []
    }

    async function open(view, data, ctxData) {
      const realParams = {
        row: data,
        ctx: ctxData,
      }
      _resetState()
      state.visible = true
      await getViewCfg(view.viewId, view.viewKey)
      const treeCfg = {
        config: treeConfig.value,
      }
      // 属于同一表单的视图
      if (view.formId === ctxData.formId) {
        treeCfg.currentTreeNode = currentTreeNode.value
        if (view.viewType === VIEW_TYPE.add) {
          // 处理复制新增业务
          if (data && data.id) {
            await getDataDetail(data.id)
          }
        }
        if (view.viewType === VIEW_TYPE.modify && data) {
          await getDataDetail(data.id)
        }
        if (view.viewType === VIEW_TYPE.detail && data) {
          await getDataDetail(data.id)
        }
      } else {
        // 非相同表单的视图
        if (view.viewType === VIEW_TYPE.add) {
          // 根据映射配置使用行数据填充新增视图
          const fillData = buildFillData(view.viewParams.saveFields, realParams)
          cacheData.value = { ...fillData }
        }
        if (view.viewType === VIEW_TYPE.detail) {
          treeCfg.currentTreeNode = null
          let id = data.id
          view.viewParams.assoFields.forEach(item => {
            id = realParams[item.scope][item.from]
          })
          await getDataDetail(id)
        }
      }
      formTreeCfg.value = treeCfg
      state.render = true
      EmitsBus.on(emitKey.delSubItem, onDelSubItem)
    }

    function close() {
      _resetState()
      EmitsBus.all.clear()
    }

    // 获取视图参数配置
    async function getViewCfg(viewId, viewKey) {
      try {
        //getViewConfigDetail
        const detail = await getViewConfigDetailByKey(viewKey)
        relationConfList.value = detail.relationConfList
        console.log(
          detail.viewInfo.status === 'N'
            ? '所选视图已经被删除,请重新配置当前操作事件'
            : '视图可用',
        )
        // 保存视图信息
        viewInfo.value = deepCopy(detail.viewInfo)
        // 设置字段列表 次数需要修改
        fieldMaps.value = detail.fields.filter(i => !isEmpty(i.validValue))
        try {
          if (detail.schema) {
            widgetForm.value = JSON.parse(detail.schema)
            console.log(widgetForm.value)
          }
        } catch (e) {
          console.warn(e)
        }
      } catch (e) {
        throwError('ViewRender/getViewCfg', e)
      }
    }

    // 判断视图类型,如为修改视图,则需要获取一下原始数据信息
    async function getDataDetail(id) {
      try {
        // TODO 这里查询详情,也要判断是否需要更换查询子表的接口,并对cacheData进行不同的处理和拼接
        const { subList } = fieldCfg.value
        const hasSub = subList.length > 0
        const fun = hasSub ? getSubDetail : getCommonDetail
        const detail = await fun({
          formId: viewInfo.value.formId,
          viewId: viewInfo.value.id,
          dataId: id,
          viewKey: viewInfo.value.viewKey,
          viewType: viewInfo.value.viewType,
        })
        // 根据需求拆分子表数据
        if (hasSub) {
          cacheData.value = { id, ...detail.mainData }
          // 组装subList
          detail.subList.forEach(item => {
            const key = item.formId
            cacheData.value[key] = deepCopy(item.dataList)
          })
        } else {
          cacheData.value = { id, ...detail }
        }
      } catch (e) {
        throwError('ViewRender/getDataDetail', e)
      }
    }

    // 统一事件处理,操作事件传参需要根据不同事件类型分别传入
    async function handleEvent(event) {
      if (event) {
        // console.log('---- action event trigger ----', event)
        if (event.key === 'submit') {
          try {
            const res = await generateFormRef.value.getFormData()
            //上传控件有必填校验
            if (uploadRequired.value.length > 0) {
              if (res.fileList) {
                for (let i = 0; i < uploadRequired.value.length; i++) {
                  if (
                    uploadRequired.value[i] in res.fileList &&
                    res.fileList[uploadRequired.value[i]].length > 0
                  ) {
                    console.log('校验通过')
                  } else {
                    Message.error(`上传附件必填`)
                    return
                  }
                }
              } else {
                //上传附件做了必填校验,而models.value.fileList为空,校验失败
                Message.error(`上传附件必填`)
                return
              }
            }
            // console.log('-----------submit-data----------------')
            const data = buildData(res)
            Store.onTriggerEvent(event, data, viewInfo.value)
          } catch (error) {
            throwError('ViewRender/handleEvent', error)
          }
        } else {
          Store.onTriggerEvent(event)
        }
      }
    }

    // 组装数据,需要判断是否包含子表内容
    function buildData(data) {
      const { fieldList, subList } = fieldCfg.value
      const hasSub = subList.length > 0
      if (hasSub) {
        // 组装mainData
        const mainData = { id: data.id, ...data }
        fieldList.forEach(item => {
          const key = item.model.toLowerCase()
          mainData[key] = data[key]
        })
        // 组装subList
        const subData = []
        subList.forEach(item => {
          const key = item.model.toLowerCase()
          const subTableRelate = relationConfList.value.find(item =>
            [item.relateFormId, item.relateTableName].includes(key),
          )
          subData.push({
            formId: subTableRelate?.relateFormId,
            tableName: subTableRelate?.relateTableName,
            dataList: data[key],
            delList: cacheSubDelList.value[key],
          })
        })
        return { ...state.cacheData, mainData, subList: subData }
      } else {
        return { ...state.cacheData, ...data }
      }
    }

    // 删除子表缓存id
    function onDelSubItem({ formId, delId }) {
      if (!cacheSubDelList.value[formId]) {
        cacheSubDelList.value[formId] = []
      }
      cacheSubDelList.value[formId].push(delId)
      console.log('on - del')
    }

    // 用于构建填充数据
    function buildFillData(saveFields, realParams) {
      const obj = {}
      saveFields.forEach(item => {
        obj[item.to] = realParams[item.scope][item.from]
      })
      return obj
    }

    provide('FieldMaps', fieldMaps) // 注入需要枚举转换的映射

    return {
      ...toRefs(state),
      generateFormRef,
      viewInfo,
      fieldMaps,
      widgetForm,
      cacheData,
      cacheSubDelList,
      open,
      close,
      handleEvent,
      loading,
      topTitle,
      currentTreeNode,
      formTreeCfg,
      uploadRequired,
      isDetailView,
      eventsArr,
    }
  },
}
</script>

<style lang="stylus">
.view-box {
  position: relative;
  width: 100%;
  min-height: 200px;
}

.bin-modal.view-modal-not-full > .bin-modal-body {
  min-height: 300px;
  max-height: 500px;
  overflow: auto;
}

.bin-modal.view-modal.is-fullscreen > .bin-modal-body {
  -webkit-box-flex: 1;
  -ms-flex: auto;
  flex: auto;
  -ms-flex-negative: 1;
  flex-shrink: 1;
  overflow: auto;
}
</style>