后台模板新增、修改组件封装

65 阅读1分钟

1. 需求描述

之前,在写后台模板新增、修改时发现这两个组件内容大致是相似的,恰好最近又在写这种,于是萌生了将这两个组件抽离出去,之后直接调用这个组件就可以使用了。

2. 代码

2.1 公共组件封装

目前使用的技术栈是 vue3 + TS。

大致思路就是将需要的数据以 formData 保存,然后将需要展示的参数内容以 formItems 的参数形式传递过来,然后使用 v-for 遍历展示。

// utils.ts
export const removeExtraCharacterOfNumber = (value: string) => {
  return value
    .replace(/[^-0-9.]/g, '')
    .replace(/^\./g, '')
    .replace('.', '$#$')
    .replace(/\./g, '')
    .replace('$#$', '.')
}
types/index.ts

export interface IFormItem {
  type: string
  kind?: string
  field: string
  label: string
  width?: number
  isNumber?: boolean
  placeholder: string
  options?: IOptionsItem[]
  slotName?: string
}

export interface IPassedDialogProps {
  title: string
  rules: FormRules
  formItems: IFormItem[]
}

export interface INewAndEditedBasic<T> {
  new: T
  edited: T
}
export interface INewAndEditedRouteParams {
  title: string
  rules: INewAndEditedBasic<FormRules>
  formItems: INewAndEditedBasic<IFormItem[]>
}
<template>
  <el-dialog
    :model-value="visible"
    :title="`${isEdited ? '修改' : '新增'}${title}`"
    @close="closeDialog"
    width="520px"
    :close-on-press-escape="false"
    :close-on-click-modal="false"
    destroy-on-close
  >
    <el-form ref="ruleFormRef" :model="formData" :rules="rules" label-width="100px">
      <template v-for="(item, index) in formItems" :key="index">
        <el-form-item :label="item.label" :label-width="item.width ? item.width : 100" class="form-item" :prop="item.field">
          <template v-if="item.type === 'input'">
            <el-input
              v-model="formData[`${item.field}`]"
              :placeholder="item.placeholder"
              :type="item.kind ? item.kind : 'text'"
              clearable
              @input="
                item.isNumber ? (formData[`${item.field}`] = handleNumberInput($event)) : null
              "
            ></el-input>
          </template>
          <template v-else-if="item.type === 'select'">
            <el-select
              v-model="formData[`${item.field}`]"
              :placeholder="item.placeholder"
              style="width: 100%"
            >
              <el-option
                v-for="option in item.options"
                :key="option.value"
                :value="option.value"
                :label="option.label"
              />
            </el-select>
          </template>
          <template v-else-if="item.type === 'slot'">
            <slot :name="item.slotName" v-bind="item" />
          </template>
        </el-form-item>
      </template>
    </el-form>
    <template #footer>
      <el-button @click="resetForm(ruleFormRef)">重置</el-button>
      <el-button type="primary" @click="submitForm(ruleFormRef)">确认</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { defineExpose, defineEmits, defineProps, PropType, ref, watch } from 'vue'
import { FormRules, FormInstance } from 'element-plus'
import { removeExtraCharacterOfNumber } from '@/utils/toolFns'
import { IFormItem } from '@/types'

// submit 提交
// 修改弹窗开关 updata:visible
// 修改表单数据 update:formData
const emits = defineEmits(['submit', 'update:visible', 'update:formData'])
const props = defineProps({
  // 是否修改
  isEdited: { type: Boolean, default: false },
  // 弹窗开关
  visible: { type: Boolean, default: false },

  // 弹窗标题
  title: { type: String, default: '' },

  // 表单验证规则
  rules: {
    type: Object as PropType<FormRules>,
    default: () => {
      return {}
    }
  },

  // 表单信息
  formItems: {
    type: Array as PropType<IFormItem[]>,
    default: () => []
  },

  // 表单绑定的内容,最后就是把这个提交给后台
  formData: {
    type: Object,
    default: () => ({})
  }
})

const ruleFormRef = ref<FormInstance>()

let formData = ref(props.formData)

const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  await formEl.validate((valid) => {
    if (valid) {
      emits('update:formData', formData.value)
      emits('submit')
      // 如果需要在对话框中添加其他判断逻辑时,那么以下代码不能添加,
      // 否则不管怎么样,下面的事件执行顺序总是会先于对话框中点击确定里面的判断逻辑(需要向后端发送请求)
      // 如果想要使用 Promise 异步操作,在上述 submit 执行结束后再根据条件是否选择执行下述代码,
      // 会发现第一次触发逻辑后不管成功与否还是会关闭对话框,这种方式暂未找到最优解
      // 目前采用的方式是在执行完对话框中的确认逻辑后手动关闭
      // emits('update:visible', false)
    }
  })
}
// 重置表单
const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.resetFields()
}

// 关闭弹窗
const closeDialog = () => {
  emits('update:visible', false)
  resetForm(ruleFormRef.value)
}

watch(
  () => props.formData,
  (val) => {
    formData.value = val
  }
)

// 由于输入时可能会有数字,所以需要对数字做处理
const handleNumberInput = (inputValue: string) => {
  return removeExtraCharacterOfNumber(inputValue)
}


</script>

<style lang="scss" scoped></style>

2.2 父组件的使用

// index.ts
const newPreApplicationFormItems = [
  {
    type: 'input',
    field: 'code',
    label: '编号',
    placeholder: '请输入编号'
  },
  {
    type: 'input',
    field: 'name',
    label: '名称',
    placeholder: '请输入名称'
  }
]
const editedPreApplicationFormItems = [
  { type: 'input', field: 'name', label: '名称', placeholder: '请输入名称' }
]
const newAndEditedFormData: INewAndEditedRouteParams = {
    title: '预申请类型',
    rules: {
      new: newPreApplicationTypeRule,
      edited: editedPreApplicationTypeRule
    },
    formItems: {
      new: [
        {
          type: 'input',
          field: 'code',
          label: '编号',
          placeholder: '请输入编号'
        },
        {
          type: 'input',
          field: 'name',
          label: '名称',
          placeholder: '请输入名称'
        }
      ],
      edited: [{ type: 'input', field: 'name', label: '名称', placeholder: '请输入名称' }]
    }
  }
<template>

...
<el-table-column
  v-if="rightsBtn.modify || rightsBtn.delete"
  fixed="right"
  label="操作"
  align="center"
  width="180"
>
  <template #default="scope">
    <tool-tip v-if="rightsBtn.modify" content="修改">
      <template #cmp>
        <el-button
          class="primary-bg-btn"
          type="primary"
          icon="Edit"
          size="small"
          @click="editType(scope.row)"
        />
      </template>
    </tool-tip>
    <tool-tip v-if="rightsBtn.delete" content="删除">
      <template #cmp>
        <el-button
          class="danger-bg-btn"
          type="danger"
          icon="Delete"
          size="small"
          @click="deleteType(scope.row)"
        />
      </template>
    </tool-tip>
  </template>
</el-table-column>
...

<PageModel
  ref="pageContentRef"
  v-bind="modelConfig"
  :is-edited="isEdited"
  :visible="addFormDialogVisible"
  :form-data="addSignFormData"
  @submit="confirmSubmit"
  @update:visible="updateDialogVisible"
  @update:form-data="updateFormData"
></PageModel>
</template>

<script setup lang="ts">
import newAndEditedFormData from './index'
import PageModel from '@/components/ComposedPage/PageModel.vue'


const isEdited = ref(false)
const addSignFormDefaultData = ref<INewDefaultForm>({
  name: '',
  code: ''
})
const addSignFormData = ref<INewDefaultForm>({
  name: '',
  code: ''
})
// 配置参数
const modelConfig = reactive<IPassedDialogProps>({
  title: '',
  rules: {},
  formItems: []
})
const addFormDialogVisible = ref(false)

const addType = () => {
  isEdited.value = false
  formDialogVisible(false)
}
const editType = (row: ICommonNameId) => {
  isEdited.value = true
  const { id, name } = row

  formDialogVisible(true, { id, name })
}

// 由于我的项目中是5、6中类型页面共用此组件,需要根据路由跳转来判断当前页面是哪种类型,
// 从而在 activated 周期中需要重置,因此将此代码抽离出来,如果只要一个页面用来此组件,可以不将代码抽离出来
const resetModelConfig = (isEdited: boolean, newAndEditedFormData: INewAndEditedRouteParams) => {
  modelConfig.title = newAndEditedFormData!.title
  modelConfig.formItems = isEdited ? newAndEditedFormData!.formItems['edited'] : newAndEditedFormData!.formItems['new']
  modelConfig.rules = isEdited ? newAndEditedFormData!.rules['edited'] : newAndEditedFormData!.rules['new']
}

// 根据条件的不同从而渲染不同的数据
const formDialogVisible = (isEdited: boolean, item?: any) => {
  if (isEdited) {
    addSignFormData.value = { ...item }
  } else {
    addSignFormData.value = { ...addSignFormDefaultData.value }
  }
  resetModelConfig(isEdited, newAndEditedFormData)
  addFormDialogVisible.value = true
}

const updateDialogVisible = (payload: boolean) => {
  addFormDialogVisible.value = payload
}
const updateFormData = (submitData: any) => {
  addSignFormData.value = submitData
}
const confirmSubmit = async () => {
  let result: IDataType<null>
  if (isEdited.value) {
    result = await editTypeManagement(addSignFormData.value as any)
  } else {
    result = await createTypeManagement(addSignFormData.value)
  }
  if (result.code !== 200) return error(result.message)
  success(result.message)
  queryList()
}
</script>

代码大致就是这样,后续还会继续优化此代码,加入其他的一些边界判断。此外,表格内容也可以抽成组件,后续有时间会将 el-table 进行二次封装。