Vue3中封装一个对话框编辑组件

2,132 阅读4分钟

常规管理系统中,一般都有大量的增删改查操作,而增和改两个操作,除了单独建立页面,很多时候都需要通过对话框+表单的形式进行,如下图: image.png

image.png

常规解决方案,就是在需要使用的地方,通过dialog组件嵌套from表单组件实现,以element-ui为例:

// 省略参数配置等
  <el-dialog>
    <el-form>
      <el-form-item></el-form-item>
    </el-form>
  </el-dialog>

这样使用的问题在于,如果使用的地方很多,那么需要在每个地方都复制同样的代码,并且配置所有的参数,显然更好的方式是做一个封装,新建一个DialogFrom组件:

// 省略参数配置等
  <el-dialog>
    <el-form>
      <el-form-item v-for="item in formItems"
        :key="item.key"
        :label="item.label"
        :prop="item.key">
        <!-- 通过判断item的type展示不同的表单项 -->
        <el-select v-if="item.type=='select'"
          v-model="form[item.key]"
          style="width: 100%">
          <el-option v-for="op in item.options"
            :key="op.value"
            :label="op.label"
            :value="op.value"></el-option>
        </el-select>
        ...
        <el-input v-else
          v-model="form[item.key]"
          :disabled="item.disabled || (itemData && item.editDisabled)"
          :type="item.type || 'text'"
          :placeholder="item.placeholder">
        </el-input>
      </el-form-item>
    </el-form>
  </el-dialog>
export default {
  props: {
    visible: {
      type: Boolean,
      required: true,
    },
    formItems: {
      type: Array,
      required: true,
    },
    itemData: {
      type: Object,
    },
    propTitle: {
      type: String,
    },
  },
  emits: ['close', 'dialog-submit'],
  setup(props, { emit }) {
    const form = reactive({})
    const title = ref('编辑')
    watch(() => props.visible, () => {
      if (props.visible == true) {
        if (props.itemData) {
          title.value = '编辑'
        } else {
          title.value = '添加'
        }
        props.formItems.map((item) => {
          if (props.itemData && props.itemData[item.key] !== null) {
            form[item.key] = props.itemData[item.key]
          } else {
            form[item.key] = ''
          }
        })
      } else {
        Object.keys(form).map((key) => {
          delete form[key]
        })
      }
    })
    const close = () => {
      emit('close')
    }
    const ruleForm = ref()
    const saveEdit = async () => {
      ruleForm.value.validate((validte) => {
        if (validte) {
          emit('dialog-submit', form)
        } else {
          ElMessage.warning('请完善表格')
        }
      })
    }
    return {
      title,
      close,
      saveEdit,
      form,
      ruleForm,
    }
  },

具体实现没有太大难度,核心就在于几个参数:

  • visible:在父组件使用时,控制弹框的展示
  • formItems:配置表单中需要展示的表单项,这里可以通过传参,对表单项进行各种配置,具体看业务需求:
      [
        { label: '用户姓名', key: 'realName', required: true },
        {
          label: '手机号(登录账号)',
          key: 'phone',
          required: true,
          placeholder: '',
          rule: phoneReg
        },
      ]
  • itemData: 主要用于编辑时,复现原数据,也需要根据这个参数是否有值,判断当前是编辑还是添加
  • propTitle:定制化标题,比如编辑xxx
  • 其他需要配置的参数,根据业务需求定制

实际使用中:

<template>
  <div>
    <DialogForm :visible="editVisible"
      @close="editVisible = false"
      :itemData="editItemData"
      :formItems="formItems"
      @dialog-submit="editSubmit"></DialogForm>
  </div>
</template>

<script>
import DialogForm from './DialogForm.vue'

export default {
  components: {
    DialogForm,
  },
  setup() {
    // 表格编辑时弹窗和保存
    const editVisible = ref(false)
    let editItemData = ref(null)
    const handleEdit = (row) => {
      editVisible.value = true
      editItemData.value = row
    }
    const handleAdd = () => {
      editVisible.value = true
      editItemData.value = null
    }
    const editSubmit = async (row) => {
      // 发送api等..
      ElMessage.success('操作成功')
      editVisible.value = false
    }
    return {
      formItems: [
        { label: '用户姓名', key: 'realName', required: true },
        {
          label: '手机号(登录账号)',
          key: 'phone',
          required: true,
          placeholder: ''
        },
      ],
      editVisible,
      editItemData,
      handleEdit,
      handleAdd,
      editSubmit
    }
  },
}
</script>

这样就完成了第一次封装,在使用时,HTML部分可以少写很多代码。

但是经过一段时间使用,发现如果页面比较多或者一个页面有多个对话框的时候,很多页面都需要去引入组件写HTML,并且维护大量变量,感觉也不是很方便,故继续封装。

这里可以想到elementUI的Message或者Confirm等组件,都是直接在JS中调用,比较方便,参考后思路如下:

  • 利用vue3中的hooks的思想,希望在使用中直接通过useDialogForm的方式,获得dialog的vm实例,在各种情况下操作。

具体实现: 新建一个useDialogForm.js,这里建议放入components中

-- components
 |-- DialogForm
   |-- DialogForm.vue
   |-- useDialogForm.js

useDialogForm.js:


// 这里的实现主要参考了elementUI中message等组件的实现
// 需要注意的是跟Vue2中写法略有区别,主要是因为一些api的改变

import {
  createApp
} from 'vue'
import DialogForm from './DialogForm.vue'
import installElementPlus from '../plugins/element'

let instance
const useFormDialog = function(config) {
  // 通过createApp可以得到一个vue的实例,这里的config约等于传入组件的props
  instance = createApp(DialogForm, {
    ...config
  })
  // 这里需要注意的是通过createApp是新生成vue实例,跟我们之前挂载在#app上的应用已经没有关系
  // 所以并没有注册过elementUI的组件,如果你想在这个组件中使用elementUI的组件,需要重新注册
  installElementPlus(instance)
  
  // 生成一个元素用于组件的挂载
  let node = document.createElement('div')
  let $vm = instance.mount(node)
  document.body.appendChild(node)
  
  // 这里注意,最后返回的是挂载到DOM元素之后的实例
  // 这样组件中setup等方法才会被真正调用,其中返回的方法我们才能在外层使用
  // 可以理解为图纸和真正产品的关系
  return $vm
}

export default useFormDialog

另外仔细查看可以发现,这个文件的代码可以用于生成任何组件,其跟DialogForm其实是解耦的。

同时DialogForm.vue也需要做一些改造,这里只展示核心部分代码:

...
  // props删除了visible和itemData,visible是因为可以组件自己内部控制,外界通过函数调用即可
  // 同时itemData也可以组件内部维护,通过函数传入
  // 增加了submitFunc,用于点击确认时的操作
    props: {
        formItems: {
          type: Array,
          required: true,
        },
        submitFunc: {
          type: Function,
          default: () => {console.log('no submit func')}
        },
        propTitle: {
          type: String,
        },
    },
    setup(props, { emit }) {
        // 组件内部自己维护visible,外界通过调用show函数控制对话框的展示
        // 同时传入itemData,通过判断是否有值来确认是编辑还是添加
        const visible = ref(false)
        const itemData = ref(null)
        const show = (row) => {
          itemData.value = row
          visible.value = true
        }
        // 现在关闭对话框和对话框确认都不需要emit外部事件了,直接内部修改visible即可
        const close = () => {
            visible.value = false
        }
        // 点击确认直接调用传入的submitFunc即可,传入表单值
        const saveEdit = async () => {
            props.submitFunc(formData)
        }
        ...
    }
...

至此,改造完成,再来看看使用方式:

<template>
</template>

<script>
import useDialogForm from '@/components/DialogForm/DialogForm.js'

export default {
  setup() {
    const editDialog = useFormDialog({
        formItems:[
            { label: '用户姓名', key: 'realName', required: true },
            {
              label: '手机号(登录账号)',
              key: 'phone',
              required: true,
              placeholder: '',
              rule: phoneReg
            },
        ],
        submitFunc: async (formData) => {
          // 调用接口等进行其他确认操作
          ...
          editDialog.close()
        }
      })
    const handleEdit = (row) => {
      // 传入数据代表是在编辑
      editDialog.show(row)
    }
    const handleAdd = () => {
      editDialog.show()
    }
    ...
  },
}
</script>

这样看上去就清爽多了,HTML代码可以直接省略,JS中不用再维护每个dialog的展示开关和需要编辑的editData,submitFunc直接写入,也不用定义好再返回出去。

至此,封装就暂时完成了。