vue 实现动态渲染表单

132 阅读3分钟

效果图:

image.png

image.png

实现逻辑梳理:

点击新增时,表单整体数据结构,需要先请求接口,包含布尔,字典,小数,整数,文本五大类型, 点击修改时,需要将表单数据进行回显,需要在点击表格修改时,将数据传入表格中,最后根据接口表单名称和表格表单名称进行匹配,最后将值进行回显。表单的必填规则,也是读取接口数据的必填属性字段,进行校验。

新增数据结构:

image.png

修改数据结构:

image.png

完整代码:

<template>
  <div class="attribute-warehouse-dialog">
    <el-dialog :title="operateType == 'create' ? '新增数据': '修改数据'" :append-to-body="true" :visible.sync="dialogVisible" :close-on-click-modal="false" width="5.6rem" @close="closeDialog" class="customer-dialog">
      <div class="dialog-box dialog-box1">
        <div class="table">
          <el-form size="small" @submit.native.prevent :model="ruleForm" ref="ruleForm" :rules="rules" class="demo-ruleForm" label-width="0.9rem" label-position='right'>
            <el-form-item label="ts" prop="ts" class="customer-date-picker">
              <el-date-picker
                v-model="ruleForm.ts"
                type="datetime"
                placeholder="选择ts"
                value-format="yyyy-MM-dd HH:mm:ss"
                :disabled="operateType == 'edit'">
              </el-date-picker>
            </el-form-item>
            <!-- 动态生成表单项 -->
            <el-form-item v-for="(item, index) in dynamicFormItems" :key="index" :prop="item.prop" :class="item.dataType === '5' ? 'customer-date-picker' : ''">
              <template #label>
                <!-- <el-tooltip :content="item.label" placement="top" :disabled="!isLabelOverflow(item.label)"> -->
                  <span class="label-with-tooltip" :title="item.label">{{ item.label }}</span>
                <!-- </el-tooltip> -->
              </template>
              <template v-if="item.dataType === '5'">
                <el-date-picker
                  v-model="ruleForm[item.prop]"
                  type="datetime"
                  :placeholder="item.placeholder"
                  value-format="timestamp">
                </el-date-picker>
              </template>
              <template v-else-if="item.dataType === '4'">
                <el-select v-model="ruleForm[item.prop]" clearable>
                  <el-option v-for="option in getOptions(item.dataType)" :key="option.value" :label="option.dictName" :value="option.id"></el-option>
                </el-select>
              </template>
              <template v-else-if="item.dataType === '6'">
                <el-select v-model="ruleForm[item.prop]" clearable>
                  <el-option v-for="option in getOptions(item.dataType, item.enumId)" :key="option.value" :label="option.dictName" :value="option.id"></el-option>
                </el-select>
              </template>
              <template v-else>
                <el-input 
                  type="text" 
                  :placeholder="item.placeholder" 
                  v-model.trim="ruleForm[item.prop]" 
                  class="input-normal" 
                  :maxlength="item.dataType === '3' ? item.length : 32" 
                  show-word-limit>
                </el-input>
              </template>
            </el-form-item>
          </el-form>
        </div>
      </div>
      <!-- 底部按钮 -->
      <div slot="footer" class="dialog-footer">
        <el-button v-debounce="[()=>closeDialog(),`click`]" class="dialog-button">取消</el-button>
        <el-button v-if="operateType !== 'detail'" type="primary" v-debounce="[()=>confirm(),`click`]" class="btn-onelevel dialog-button">
          确认
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { updateDataSet, getMetaDataAttrByMetaDataId, addDynamicAttr } from "@/api/AIModel";
import { dictitemList } from "@/api/myapi";

export default {
  name: 'AttributeWarehouseDialog',
  data() {
    return {
      dictionaryLists: {}, // 用于存储每个 enumId 对应的字典属性列表
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() < Date.now() - 8.64e7;
        },
      },
      metadataStatus: false,
      operateType: '',
      detailData: {},
      dialogVisible: false,
      ruleForm: {
        ts: ''
      },
      DataList: [],
      BooleanList: [
        { dictName: 'true', id: 'true' },
        { dictName: 'false', id: 'false' }
      ],
      dynamicFormItems: [], // 动态生成的表单项
      rules: {
        ts: [
          {
            required: true,
            trigger: ['blur', 'change'],
            message: '选择ts',
          }]
      },
    }
  },
  methods: {
    openDialog(data, type) {
      this.operateType = type
      this.dialogVisible = true
      this.detailData = data
      this.getMoveFormData().then(() => {
        if (type == 'edit') {
          // 初始化动态字段
          this.dynamicFormItems.forEach(item => {
            if (item.dataType === '5' && item.prop !== 'ts') {
              this.$set(this.ruleForm, item.prop, new Date(data[item.prop]).getTime())
            } else {
              this.$set(this.ruleForm, item.prop, String(data[item.prop] || ''))
            }
          })
          // 将 data 数据赋值给表单,进行数据回显
          Object.keys(data).forEach(key => {
            if (this.ruleForm.hasOwnProperty(key)) {
              if (data[key] === '--') {
                data[key] = ''
              }
              if (this.dynamicFormItems.find(item => item.prop === key && item.dataType === '5' && key !== 'ts')) {
                this.$set(this.ruleForm, key, new Date(data[key]).getTime())
              } else {
                const item = this.dynamicFormItems.find(item => item.prop === key)
                // 根据详情返回的字典名称,匹配字典列表中的字典项,获取字典项的值,并进行数据回显
                if (item && (item.dataType === '4' || item.dataType === '6')) {
                  const dictionaryList = this.dictionaryLists[item.enumId] || []
                  const matchedItem = dictionaryList.find(dictItem => dictItem.dictName === String(data[key]))
                  if (matchedItem) {
                    this.$set(this.ruleForm, key, matchedItem.id)
                  } else {
                    this.$set(this.ruleForm, key, String(data[key]))
                  }
                } else {
                  this.$set(this.ruleForm, key, String(data[key]))
                }
              }
            }
          })
          // 新增逻辑:遍历 data 对象,匹配字典项列表
          Object.keys(data).forEach(key => {
            const item = this.dynamicFormItems.find(item => item.prop === key)
            if (item && (item.dataType === '4' || item.dataType === '6')) {
              const dictionaryList = this.dictionaryLists[item.enumId] || []
              const matchedItem = dictionaryList.find(dictItem => dictItem.dictName === String(data[key]))
              if (matchedItem) {
                this.$set(this.ruleForm, key, matchedItem.id)
              }
            }
          })
          // 确保 ts 字段正确回显
          if (data.ts) {
            this.$set(this.ruleForm, 'ts', data.ts)
          }
        } else {
          this.reset()
        }
      })
    },

    async getMoveFormData() {
      let { data: res } = await getMetaDataAttrByMetaDataId(this.$route.query.metadataId)
      const dictionaryPromises = res.data.map(item => {
        if (item.enumId) {
          return this.getDictionaryAttributeList(item.enumId).then(data => {
            this.$set(this.dictionaryLists, item.enumId, data)
          })
        }
        return Promise.resolve()
      })
      await Promise.all(dictionaryPromises)
      this.dynamicFormItems = res.data.map(item => ({
        length: item.length,
        label: item.name,
        prop: item.name,
        isMust: item.isMust,
        dataType: item.dataType,
        placeholder: `请输入 ${item.name}`,
        enumId: item.enumId // 添加 enumId 到 dynamicFormItems
      }))

      // 动态生成校验规则
      this.dynamicFormItems.forEach(item => {
        const rules = []
        if (item.isMust) {
          rules.unshift({
            required: true,
            trigger: ['blur', 'change'],
            message: `请输入${item.label}`,
          })
        }
        // if (item.isMust) {
          // 根据 dataType 添加不同的校验规则
          if (item.dataType == '1') { // 正整数
            rules.push({
              pattern: /^\d+$/,
              trigger: ['blur', 'change'],
              message: `请输入正整数`,
            })
          } else if (item.dataType == '2') { // 小数
            rules.push({
              pattern: /^\d*\.\d+$/,
              trigger: ['blur', 'change'],
              message: `请输入小数`,
            })
          } 
          // else if (item.dataType == '4') { // 布尔值
            // rules.push({
            //   validator: (rule, value, callback) => {
            //     if (value !== 'true' && value !== 'false') {
            //       callback(new Error('请输入true或false'))
            //     } else {
            //       callback()
            //     }
            //   },
            //   trigger: ['blur', 'change'],
            // })
          // } 
          this.$set(this.rules, item.prop, rules)
        // }
      })

      // 初始化 ruleForm 中的动态字段
      this.dynamicFormItems.forEach(item => {
        this.$set(this.ruleForm, item.prop, '')
      })
      return Promise.resolve()
    },
    // 获取字典项属性列表
    async getDictionaryAttributeList(enumId) {
      const { data: res } = await dictitemList({ dictId: enumId })
      if (res.code == '0' && res.data) {
        return res.data.map(item => ({
          id: item.itemValue,
          dictName: item.itemText
        }))
      } else {
        return []
      }
    },

    getOptions(dataType, enumId) {
      if (dataType === '4') {
        return this.BooleanList
      } else if (dataType === '6' && enumId) {
        return this.dictionaryLists[enumId] || []
      }
      return []
    },
    confirm() {
      this.$refs['ruleForm'].validate(async valid => {
        if (!valid) return;
        let res = {}
        // 过滤掉值为空字符串,或null,NaN类型的字段
        // const filteredFormData = Object.fromEntries(Object.entries(this.ruleForm).filter(([key, value]) => value !== '' && value !== null && !Number.isNaN(value)))
        res = await addDynamicAttr(this.ruleForm, this.$route.query.attributeId)
        if (res.data.code == '0') {
          this.$message({
            message: res.data.message,
            type: 'success'
          })
          this.$emit('getInit')
          this.closeDialog();
        }
      });
    },

    closeDialog() {
      this.dialogVisible = false
    },

    reset() {
      this.$nextTick(() => {
        this.ruleForm = {}
        this.dynamicFormItems.forEach(item => {
          this.$set(this.ruleForm, item.prop, '')
        })
        this.$refs.ruleForm.resetFields();
      })
    },
    isLabelOverflow(label) {
      const labelWidth = 90; // 假设label-width为0.9rem,这里假设1rem=100px
      return label.length * 10 > labelWidth; // 粗略估计每个字符宽度为10px
    },
  }
}
</script>

<style lang="scss" scoped>

/deep/ .el-dialog__body {
  padding: .2rem .2rem 0 !important;
}

.dialog-box1 {
  min-height: 1rem;
  //max-height: 6.36rem;
  box-sizing: border-box;
  position: relative;
  padding: 0 0.12rem 0 0.12rem;
  overflow: hidden;

  .demo-ruleForm {
    height: 100%;

    /deep/ .el-form-item__error {
      width: 3.14rem;
    }
  }
}

.el-checkbox {
  margin-right: 0.4rem !important;
  padding: 0.05rem 0 0.05rem;
}

.el-button--mini {
  padding: 0.1rem 0.15rem;
}

/deep/ .el-dialog__footer {
  padding: 0 0.85rem 0.42rem 0.32rem;
}

.dialog-button {
  padding: 0.09rem 0.25rem !important;
}

.table {
  max-height: 4.41rem;
  overflow-y: auto;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  word-break: normal;
  height: 0.44rem;
  padding: 0 0.1rem;
  border-bottom: 1px solid #ebebeb;
  font-size: 0.14rem;

  &:last-child {
    border-bottom: none;
  }

  .item {
    display: flex;
    align-items: center;
    width: 2.46rem;

    span {
      flex-shrink: 0;
    }

    .auto {
      margin-left: 0.1rem;
    }

    /deep/ .el-input {
      padding: 0 0.1rem;
      width: .7rem;
    }

    ::v-deep input::-webkit-outer-spin-button,
    ::v-deep input::-webkit-inner-spin-button {
      -webkit-appearance: none;
    }

    ::v-deep input[type="number"] {
      -moz-appearance: textfield;
    }

    /deep/ .el-input__inner {
      height: 0.3rem;
      line-height: 0.3rem;
    }
  }

  .item-px {
    justify-content: flex-end;

    &-w {
      width: 1.2rem
    }
  }
}

/deep/.el-textarea {
  width: 86% !important;
}

// .label-with-tooltip {
//   display: inline-block;
//   width: 90px; // 与label-width对应
//   white-space: nowrap;
//   overflow: hidden;
//   text-overflow: ellipsis;
// }

END...