Vue3+elementPlus实现动态表单和表格组件

11,125 阅读4分钟

抛出问题

在实现表单搜索页面的时候,你还是在cv然后修改字段和标签属性吗?是不是每一个存在表单的页面都写着这些重复的代码。

image-20220221091341294.png

实现展示表格数据的页面时,同样的也存在着许许多多的重复代码,仅仅是因为他们的一部分属性的不同,但是不同的页面仍然使用的是同样的代码。

  <el-table :data="tableData" style="width: 100%" height="250">
    <el-table-column fixed prop="date" label="Date" width="150" />
    <el-table-column prop="name" label="Name" width="120" />
    <el-table-column prop="state" label="State" width="120" />
    <el-table-column prop="city" label="City" width="320" />
    <el-table-column prop="address" label="Address" width="600" />
    <el-table-column prop="zip" label="Zip" width="120" />
  </el-table>

我们是不是应该简化自己的操作,来实现个高复用的组件,让我们实现搜索表单或者表格的时候,能够通过配置来完成想要的内容。

注意: 对组件不太熟练的建议先看 vue3组件的使用

完整代码传送门 form 和 table

说干就干

1. 提炼出表单的配置项(公共部分)

根据上面的截图来看,每一个表单项不同的就是内容标签类型、标签的属性,还有就是表单项的label属性,每一个表单项需要使用v-model进行数据的双向绑定。

因此我们能够将form-item的配置提取出来,基本的内容配置就配置好了。

type IFormType = "input" | "password" | "select" | "datepicker"type IOptions = {
  label: string
  value: string
}
​
export interface IFormItem {
  field?: string  // v-model绑定到对象对应的属性
  label: string
  placeholder: any
  type: IFormType
  options?: IOptions[] // 下拉框属性
  // 其他想要配置也可以配置
}

表单项的内容配置好了,同样表单的包裹标签也需要对应的diy属性,例如:样式,表单项之间的距离等。

export interface IForm {
  formItems: IFormItem[] // 表单内容
  labelWidth?: string   
  colLayout?: any   // col标签的占位
  itemStyle?: any   // 表单项的样式
}

根据这些配置,让我们根据配置完成表单的代码实现

  1. 配置props,限制父组件传值的类型并设置可不传内容的默认内容
  props: {
    formData: {
      type: Object,
      required: true
    },
    formItems: {
      type: Array as PropType<IFormItem[]>,
      default: () => []
    },
    labelWidth: {
      type: String,
      default: () => "100px"
    },
    itemStyle: {
      type: Object,
      default: () => ({ padding: "10px 4px" })
    },
    colLayout: {
      type: Object,
      default: () => ({
        xl: 6,
        lg: 8,
        md: 12,
        sm: 24,
        xs: 24
      })
    }
  },
  1. 根据formItems的配置项,对表单项进行动态遍历渲染;绑定对应的属性。
    <el-form :label-width="labelWidth">
      <el-row>
        <template v-for="item in formItems" :key="item.label">
          <el-col v-bind="colLayout">
            <el-form-item :label="item.label" :style="itemStyle">
              <template
                v-if="item.type === 'input' || item.type === 'password'"
              >
                <el-input
                  :placeholder="item.placeholder"
                  :show-password="item.type === 'password'"
                  v-model="formData[`${item.field}`]"
                ></el-input>
              </template>
              <template v-else-if="item.type === 'select'">
                <el-select
                  :placeholder="item.placeholder"
                  style="width: 100%"
                  v-model="formData[`${item.field}`]"
                >
                  <el-option
                    v-for="option in item.options"
                    :key="option.value"
                    :value="option.value"
                  >
                    {{ option.label }}
                  </el-option>
                </el-select>
              </template>
              <template v-else-if="item.type === 'datepicker'">
                <el-date-picker
                  v-bind="item.otherOptions"
                  v-model="formData[`${item.field}`]"
                ></el-date-picker>
              </template>
            </el-form-item>
          </el-col>
        </template>
      </el-row>
    </el-form>

这样就能够渲染出我们想要的表单,同样想更diy实现,可以添加header和footer两个插槽,让组件更定制化。

  <div class="sz-form">
    <div class="header">
      <slot name="header"></slot>
    </div>
    <el-form :label-width="labelWidth">
      ...
    </el-form>
    <div class="footer">
      <slot name="footer"></slot>
    </div>
  </div>

实现效果如下:

    <!-- 使用组件的数据双向绑定, 组件内默认使用modelValue接收 -->
    <sz-form v-bind="formConfig" :formData="formData">
      <template #header>
        <h2>高级检索</h2>
      </template>
      <template #footer>
        <div class="handleBtn">
          <el-button icon="el-icon-refresh">重置</el-button>
          <el-button
            type="primary"
            icon="el-icon-search"
            >搜索</el-button
          >
        </div>
      </template>
    </sz-form>

image-20220221100241716.png

2. 提炼出表格的配置项

由elementPlus的表格项显然能看出,表各项都需要绑定到数据对象的属性,label还需要写好对应的宽度width;因此可以写成这样:

    <el-table
      class="content"
      border
      :data="tableData"
      style="width: 100%"
    >
      <template v-for="item in propList" :key="item.prop">
        <el-table-column v-bind="item" align="center" show-overflow-tooltip>
          {{ item.prop }}
        </el-table-column>
      </template>
    </el-table>

可这样,真的完成了吗? 当需要在表单项内部将对应的属性变成其他样式显示,并不是单纯显示其字段,又或是不是放入属性的元素,而是放入自定义的内容;例如:按钮图片等。

这个时候就需要我们写好插槽,但是不同的表单存在不同的属性,这让插槽实现有存在着困难,如何解决的呢?

当我们不需要diy时,就是用默认显示文本属性,当需要我们给定插槽名,让我们的配置去定义插槽的名字(插槽的name也可以进行绑定)。

因此,我们需要将 {{ item.prop }} 改成插槽形式:第一个template #default是elementPlus内部的插槽,scope.row就是属于该行的对象。

      <template v-for="item in propList" :key="item.prop">
        <el-table-column v-bind="item" align="center" show-overflow-tooltip>
          <template #default="scope">
            <slot :name="item.slotName" :row="scope.row">{{
              scope.row[item.prop]
            }}</slot>
          </template>
        </el-table-column>
      </template>

给配置项中添加上slotName属性标志使用的插槽名

export interface propItem {
  prop?: string,
  label: string,
  minWidth: string,
  slotName?: string
}

同样我们和表单组件一样,给表格组件添加上header和footer组件

  <div class="table">
    <div class="header">
      <slot name="header">
        <div class="title">{{ title }}</div>
        <div class="handler">
          <slot name="headerHandler"></slot>
        </div>
      </slot>
    </div>
    <el-table>
      ...
    </el-table>
    <div class="footer">
      <slot name="footer">
        <!-- 默认可以给个分页组件 -->
      </slot>
    </div>
  </div>

image-20220221105106325.png

写到这里就能够自定义的实现表格内部的diy,但是这其中似乎还能够抽取出公共的部分,时间部分还有按钮盒子,状态显示,似乎很多表格都需要写到,因此我们可以再次抽取一层组件,让这层组件实现一些默认插槽的实现;同样也制定一个插槽slot给上层父组件自定义插槽。 设置默认插槽数组,和传入进来propList进行匹配

    // 默认插槽数组
    const defaultSlots = [
      "headerHandler",
      "status",
      "createdTime",
      "updateTime",
      "manage"
    ]
    const diySlots = props.contentTableConfig?.propList.filter((item: any) => {
      if (defaultSlots.includes(item.slotName)) return false
      else return true
    })
    // 非默认的插槽数组
    const otherSlotsList = ref(diySlots)

代码实现:

  <sz-table
    :tableData="tableData"
    :pageName="pageName"
    :total="total"
    v-bind="contentTableConfig"
    v-model:pageInfo="pageInfo"
  >
    <!-- 1. 默认功能插槽 -->
    <!-- 上面图中的代码,默认有的功能 -->
    
    <!-- 2. 自定义插槽 -->
    <template
      v-for="item in otherSlotsList"
      :key="item.prop"
      #[item.slotName]="scope"
    >
      <template v-if="item.slotName">
        <slot :name="item.slotName" :row="scope.row"></slot>
      </template>
    </template>
  </sz-table>

到这里,我们算是完成了表格组件的配置实现;下面根据示例来看看吧

示例:实现商品信息的查看

  <div class="goods">
    <page-content :content-table-config="tableConfig" page-name="goods">
      <template #image="scope">
        <el-image
          style="width: 60px; height: 100px"
          :src="scope.row.imgUrl"
          :preview-src-list="[scope.row.imgUrl]"
        ></el-image>
      </template>
      <template #oldPrice="scope"> ¥ {{ scope.row.oldPrice }} </template>
      <template #newPrice="scope"> ¥ {{ scope.row.newPrice }} </template>
    </page-content>
  </div>

image-20220221110635056.png

总结

抽取组件的时候虽然比cv来的复杂,但是做许多相同且繁琐的事情,这其中肯定是有规律可以找的,找到规律并实现逻辑,这样以后每一次遇到写表单和表格的时候,就方便了许多,只需要配置项就能够获得简单的内容;也能够通过插槽来获得diy的内容。

王红元老师说:慢慢地一步步做好,这便就是快。

不要嫌抽组件麻烦,只能一时,后面使用的时候就是便捷了。