基于element-plus实现vue3+ts后台管理系统的组件封装(只需传入配置对象,就可以渲染出一个页面(表单+表格))

el-form组件的二次封装

我们知道el-form-item组件需要传入一个基础的属性。

  • label: 表示表单每一项的标题。
  • rules: 表单验证配置
  • prop: 提供了rules,就需要配置该属性,他的值是每项绑定的v-model的属性名。
  • style: 表单样式控制

下面我们写出每项表单的类型约束。

    type IFormType = 'input' | 'password' | 'select' | 'datepicker'
    export interface IFormItem {
      // 来获取对应表单项的数据,我们会将表单全部数据,通过Object传递,所以需要他动态获取。
      field: string
      // formitem的选项,表单类型
      type: IFormType
      // 表单项的文本
      label: string
      // 表单验证规则
      rules?: any[]
      // 表单的提示文字
      placeholder?: any
      // 针对select
      options?: any[]
      // 针对特殊的属性
      otherOptions?: any
      // 是否显示该表单项
      isHidden?: boolean
    }
    
    // 整个表单的props对象的约束。
    export interface IForm {
      // 表单的每一项配置
      formItems: IFormItem[]
      // label的宽度
      labelWidth?: string
      // 通过el-row, el-col做响应式
      colLayout?: any
      // 每个表单项的样式
      itemStyle?: any
    }
复制代码

我们还提供了头部插槽和尾部插槽,用来实现对头部和尾部的不同定制。他就是对传入的配置项,根据type字段做判断,来渲染不同的表单控件。

需要传递的props

props: {
    // 表单数据绑定
    modelValue: {
      type: Object,
      required: true
    },
    // 表单渲染的每一项
    formItems: {
      type: Array as PropType<IFormItem[]>,
      default: () => {
        return []
      }
    },
    // 表单标题的宽度
    labelWidth: {
      type: String,
      default: '100px'
    },
    // 每项表单的样式
    itemStyle: {
      type: Object,
      default: () => {
        return {
          padding: '10px 40px'
        }
      }
    },
    // 为表单做布局
    colLayout: {
      type: Object,
      default: () => ({
        xl: 6, // >1920px 4个
        lg: 8,
        md: 12,
        sm: 24,
        xs: 24
      })
    }
  },
复制代码

组件的封装

<template>
  <div class="zh-form">
    <div class="form-header">
      <slot name="header"></slot>
    </div>
    <el-form :label-width="labelWidth">
      <el-row>
        <template v-for="formItem in formItems" :key="formItem.label">
          <el-col v-bind="colLayout">
            <!-- 通过条件判断来渲染不同的表单 -->
            <el-form-item
              :label="formItem.label"
              :rules="formItem.rules"
              :style="itemStyle"
              :prop="formItem.field"
              v-if="!formItem.isHidden"
            >
              <!-- 渲染普通input和password -->
              <template
                v-if="formItem.type === 'input' || formItem.type === 'password'"
              >
                <el-input
                  :type="formItem.type"
                  :placeholder="formItem.placeholder"
                  v-model="formValues[`${formItem.field}`]"
                ></el-input>
              </template>
              <!-- 渲染select表单 -->
              <template v-if="formItem.type === 'select'">
                <el-select
                  :placeholder="formItem.placeholder"
                  v-model="formValues[`${formItem.field}`]"
                >
                  <el-option
                    v-for="optionItem in formItem.options"
                    :key="optionItem.title"
                    :label="optionItem.title"
                    :value="optionItem.value"
                  ></el-option>
                </el-select>
              </template>
              <!-- 渲染date表单 -->
              <template v-if="formItem.type === 'datepicker'">
                <el-date-picker
                  v-bind="formItem.otherOptions"
                  style="width: 100%"
                  v-model="formValues[`${formItem.field}`]"
                ></el-date-picker>
              </template>
            </el-form-item>
          </el-col>
        </template>
      </el-row>
    </el-form>
    <div class="form-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue'
import { IFormItem } from './type'

export default defineComponent({
  props: {
    modelValue: {
      type: Object,
      required: true
    },
    formItems: {
      type: Array as PropType<IFormItem[]>,
      default: () => {
        return []
      }
    },
    labelWidth: {
      type: String,
      default: '100px'
    },
    itemStyle: {
      type: Object,
      default: () => {
        return {
          padding: '10px 40px'
        }
      }
    },
    colLayout: {
      type: Object,
      default: () => ({
        xl: 6, // >1920px 4个
        lg: 8,
        md: 12,
        sm: 24,
        xs: 24
      })
    }
  },
  setup(props, { emit }) {
    // 这里取出表单数据的拷贝,如果修改表单数据的时候,即清空数据,由于这里的代码只能执行一次,所以不能被清除。我们可以使用watch来监听,然后当表单数据清空后,我们就可以重新拷贝了。
    const formValues = ref({ ...props.modelValue })
    
    // watch(
    //   () => props.modelValue,
    //   (newModelValue) => {
    //     formValues.value = newModelValue
    //   }
    // )
    
    watch(
      formValues,
      (newFormValues) => {
        emit('update:modelValue', newFormValues)
      },
      {
        deep: true
      }
    )
    return {
      formValues
    }
  }
})
</script>
<style scoped lang="less">
.zh-form {
  padding: 20px 10px;
  .form-header {
    text-align: center;
  }
  .form-footer {
    text-align: right;
  }
}
</style>
复制代码

为了使用方便,我们又对封装的Form组件做了一层抽离。

// page-search.vue
<template>
  <div class="page-search">
    <zh-form v-bind="formOptions" v-model="formValues">
      <template #header>
        <h1 class="header">高级检索</h1>
      </template>
      <template #footer>
        <el-button plain size="medium" type="danger" @click="handleClear"
          >重置</el-button
        >
        <el-button plain size="medium" type="primary" @click="handleSearch"
          >搜索</el-button
        >
      </template>
    </zh-form>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue'
import ZhForm from '../../../base-ui/Form'
import { IForm } from '../../../base-ui/Form/src/type'

export default defineComponent({
  props: {
    formOptions: {
      type: Object as PropType<IForm>,
      required: true
    }
  },
  components: {
    ZhForm
  },
  emits: ['handleClear', 'handleSearch'],
  setup(props, { emit }) {
    const originFormValues: any = {}
    props.formOptions.formItems.forEach((item) => {
      originFormValues[`${item.field}`] = ''
    })
    // 定义表单相关的数据。
    const formValues = ref(originFormValues)
    // 处理清空按钮
    const handleClear = () => {
      // formValues.value = originFormValues
      for (const key in originFormValues) {
        formValues.value[`${key}`] = originFormValues[key]
      }

      // 将事件传入父组件
      emit('handleClear')
    }

    // 处理搜索
    const handleSearch = () => {
      emit('handleSearch', formValues.value)
    }
    return {
      formValues,
      handleClear,
      handleSearch
    }
  }
})
</script>

复制代码

下面我们就可以传入一个配置文件,就渲染出来一个表单了。

我们来举个例子吧。

// 配置文件ts
import { IForm, IFormItem } from '@/base-ui/Form/src/type'
const formItemOptions: IFormItem[] = [
  {
    field: 'name',
    label: '角色名',
    type: 'input',
    placeholder: '请输入角色名',
    rules: [
      {
        required: true,
        message: 'Please input Activity name',
        trigger: 'blur'
      },
      {
        min: 3,
        max: 5,
        message: 'Length should be 3 to 5',
        trigger: 'blur'
      }
    ]
  },
  {
    field: 'time',
    label: '根据时间选择内容',
    type: 'datepicker',
    otherOptions: {
      startPlaceholder: '开始时间',
      endPlaceholder: '结束时间',
      type: 'daterange'
    }
  }
]

const formOptions: IForm = {
  formItems: formItemOptions,
  labelWidth: '120px',
  colLayout: {
    xl: 6, // >1920px 4个
    lg: 8,
    md: 12,
    sm: 24,
    xs: 24
  }
  // itemStyle: { padding: '20px 40px' }
}

export default formOptions
复制代码
<template>
  <div class="test">
    <page-search :formOptions="formOptions"></page-search>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import { PageSearch } from '../../../../components/PageSearch'
export default defineComponent({
  components: {
    PageSearch
  },
  setup() {
    return {
      formOptions
    }
  }
})
</script>
复制代码

下面就可以配制出自定义的表单组建了,是不是很方便。下面就来看看效果吧。

表单1.gif 其实上面组件还存在一个小问题,就是做表单双向绑定的时候,不好理解。我们还有一种方法,就是自己实现v-model, 就是我们在使用element-plus表单组件的时候,不通过v-model绑定数据,我们自己通过modelValue和事件update:modelValue($event, formItem.field)实现 这样就比较容易理解。下面来看看吧。

<el-input
  :type="formItem.type"
  :placeholder="formItem.placeholder"
  :modelValue="modelValue[`${formItem.field}`]"
  @update:modelValue="handleValueChange($event, formItem.field)"
></el-input>

const handleValueChange = (value: any, field: string) {
    emit("update:modelValue", {...props.modelValue, [field]: value})
}
复制代码

对el-table组件的二次封装

这个表格组件封装需要考虑很多东西,比较复杂。他主要是对插槽的处理。因为我们每次渲染的时候,每列的内容可能都不一样,所以他主要是提供插槽,然后外界实现。

对于el-table组件他只需要绑定列表数据即可。

对于el-table-column它需要提供一个属性。

  • prop:对应数据项的字段。如果不提供将不会渲染数据。
  • label: 提供每一列的标题。

我们还需要对一个非数据项做处理。

  • 选中列
  • 编号列
  • 操作列

前两项可以通过el-table-column内置的属性完成,最后一项,我们可以通过传入配置项完成。

下面来看看table组件的props吧。

props: {
    // 数据列表
    tableData: {
      type: Array,
      required: true
    },
    // el-table-column配置项
    propList: {
      type: Array,
      required: true
    },
    // 是否具有编号列
    isNumberColumn: {
      type: Boolean,
      default: false
    },
    // 是否具有选中列
    isSelectColumn: {
      type: Boolean,
      default: false
    },
    // 列表标题
    listTitle: {
      type: String,
      required: true
    },
    // 数据条数
    tableDataCount: {
      type: Number,
      default: 0
    },
    // 分页参数
    page: {
      type: Object,
      default: () => ({ currentPage: 0, pageSize: 10 })
    }
复制代码

组件封装

<template>
  <div class="contain-table">
    <!-- 列表头部组件插槽 -->
    <div class="list-header">
      <slot name="header">
        <!-- 提供默认的插槽内容 -->
        <div class="list-title">{{ listTitle }}</div>
        <div class="list-operate">
          <slot name="listOperate"></slot>
        </div>
      </slot>
    </div>
    <el-table :data="tableData" border style="width: 100%">
      <!-- 对选中列进行封装 -->
      <el-table-column
        v-if="isSelectColumn"
        type="selection"
        width="60"
        align="center"
      ></el-table-column>
      <!-- 对于编号栏是否存在进行封装 -->
      <el-table-column
        v-if="isNumberColumn"
        type="index"
        label="编号"
        width="80"
        align="center"
      ></el-table-column>
      <!-- 数据列表项的渲染,提供插槽。 -->
      <template v-for="prop in propList" :key="prop.prop">
        <el-table-column v-bind="prop" align="center" show-overflow-tooltip>
          <template #default="scope">
            <slot :name="prop.slotName" :row="scope.row">
              {{ scope.row[prop.prop] }}
            </slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
    <!-- 列表尾部组件插槽 , 默认是分页器-->
    <div class="list-footer">
      <slot name="listFooter">
        <el-pagination
          :currentPage="page.currentPage"
          :page-size="page.pageSize"
          layout="total, prev, pager, next"
          :total="tableDataCount"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        >
        </el-pagination>
      </slot>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    tableData: {
      type: Array,
      required: true
    },
    propList: {
      type: Array,
      required: true
    },
    // 是否具有编号列
    isNumberColumn: {
      type: Boolean,
      default: false
    },
    // 是否具有选中列
    isSelectColumn: {
      type: Boolean,
      default: false
    },
    // 列表标题
    listTitle: {
      type: String,
      required: true
    },
    tableDataCount: {
      type: Number,
      default: 0
    },
    // 分页参数
    page: {
      type: Object,
      default: () => ({ currentPage: 0, pageSize: 10 })
    }
  },
  emits: ['update:page'],
  setup(props, { emit }) {
    // 处理分页器
    const handleCurrentChange = (currentPage: number) => {
      emit('update:page', { ...props.page, currentPage })
    }
    const handleSizeChange = (pageSize: number) => {
      emit('update:page', { ...props.page, pageSize })
    }
    return {
      handleCurrentChange,
      handleSizeChange
    }
  }
})
</script>
<style scoped lang="less">
.list-header {
  display: flex;
  height: 45px;
  padding: 0 10px;
  justify-content: space-between;
  align-items: center;

  .list-title {
    font-size: 20px;
    font-weight: 700;
  }

  .list-operate {
    align-items: center;
  }
}

.list-footer {
  display: flex;
  justify-content: flex-end;
  width: 100%;
  margin-top: 20px;
}
</style>
复制代码

为了使用方便,我们又对封装的Table组件做了一层抽离。其实这一层就是实现相同的内容,不同的内容到页面组件中自己实现。

  • 这里主要实现的是共同的插槽内容,比如状态栏(都是用el-tag实现)。

  • 如果需要提供不同的插槽内容(例如展示图片,我们不可能都在这一层组件实现,这样实现的插槽太多,而且这只是个别组件才会实现的,这时候就需要在页面组件中自己实现了。),我们还需要提供插槽,这里就涉及到了跨组件插槽的传递了。

  • 我们需要根据传入的propList中的slotName过滤,然后来提供插槽。

<template>
  <div class="page-contain-table">
    <zh-contain-table
      :tableData="tableData"
      v-bind="containTableConfig"
      :tableDataCount="tableDataCount"
      v-model:page="pageInfo"
    >
      <!-- 列表头部插槽的实现 -->
      <template #listOperate>
        <el-button
          type="primary"
          size="medium"
          plain
          @click="handleAddListItem"
          >{{ tableOperateTitle }}</el-button
        >
      </template>
      <!-- 状态插槽 -->
      <template #status="scope">
        <el-tag type="success" v-if="scope.row.status === 1">启用</el-tag>
        <el-tag type="info" v-else>禁用</el-tag>
      </template>
      <!-- 传入操作列插槽内容 -->
      <template #operateColumn="scope">
        <el-button
          plain
          size="mini"
          type="primary"
          @click="handleEditModalDialog(scope.row)"
          >编辑</el-button
        >
        <el-button plain size="mini" type="danger">删除</el-button>
      </template>
      <!-- 提供插槽,为了实现个性化配置 -->
      <template
        v-for="item in otherPropSlots"
        :key="item.prop"
        #[item.slotName]="scope"
      >
        <template v-if="item.slotName">
          <slot :name="item.slotName" :row="scope.row"></slot>
        </template>
      </template>
    </zh-contain-table>
  </div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import ZhContainTable from '../../../base-ui/ContainTable'
import { useStore } from '../../../store'

export default defineComponent({
  props: {
    containTableConfig: {
      type: Object,
      required: true
    },
    // 用于请求每个菜单的数据,所以传入一个pageName
    pageName: {
      type: String,
      required: true
    },
    tableOperateTitle: {
      type: String,
      default: '添加用户'
    }
  },
  emits: ['handleAddListItem', 'editListItem'],
  components: {
    ZhContainTable
  },
  setup(props, { emit }) {
    const store = useStore()
    const pageInfo = ref({
      pageSize: 5,
      currentPage: 1
    })

    // 当点击分页按钮时,重新请求数据
    watch(pageInfo, () => {
      getListData()
    })
    const getListData = (queryInfo: any = {}) => {
      store.dispatch('system/getPageListAction', {
        pageName: props.pageName,
        queryInfo: {
          offset: pageInfo.value.currentPage * pageInfo.value.pageSize,
          size: pageInfo.value.pageSize,
          ...queryInfo
        }
      })
    }
    getListData()
    // 获取列表数据
    const tableData = computed(() =>
      store.getters[`system/getPageListData`](props.pageName)
    )
    // 获取列表条数
    const tableDataCount = computed(() =>
      store.getters[`system/getPageListDataCount`](props.pageName)
    )
    // 过滤slotName
    const otherPropSlots = props.containTableConfig?.propList.filter(
      (item: any) => {
        if (item.slotName === 'status') return false
        if (item.slotName === 'createAt') return false
        if (item.slotName === 'updateAt') return false
        if (item.slotName === 'operateColumn') return false
        return true
      }
    )

    // 当点击添加列表项时
    const handleAddListItem = () => {
      emit('handleAddListItem')
    }

    // 当点击编辑按钮的时
    const handleEditModalDialog = (item: any) => {
      emit('editListItem', item)
    }

    return {
      tableData,
      tableDataCount,
      getListData,
      pageInfo,
      otherPropSlots,
      handleAddListItem,
      handleEditModalDialog
    }
  }
})
</script>
<style scoped lang="less">
.page-contain-table {
  padding: 20px;
  border-top: 20px solid #f5f5f5;
}
</style>
复制代码

下面我们就来传入配置文件,显然出一个表格列表吧。

    // 定义需要展示哪些数据
    const propList = [
      {
        prop: 'name',
        label: '角色',
        slotName: 'name'
      },
      {
        prop: 'intro',
        label: '具有的权限',
        slotName: 'intro'
      },
      {
        prop: 'createAt',
        label: '创建时间',
        slotName: 'createAt'
      },
      {
        prop: 'updateAt',
        label: '更新时间',
        slotName: 'updateAt'
      },
      // 操作列
      {
        label: '操作',
        slotName: 'operateColumn',
        width: '150'
      }
    ]

    const containTableConfig = {
      listTitle: '角色列表',
      propList,
      isNumberColumn: true,
      isSelectColumn: true
    }

    export default containTableConfig
复制代码

页面组件,渲染页面

<template>
  <div class="test">
    <page-search :formOptions="formOptions"></page-search>
    <page-contain-table
      :containTableConfig="containTableConfig"
      pageName="role"
    ></page-contain-table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import containTableConfig from './config/containTableConfig'
import { PageSearch } from '../../../../components/PageSearch'
import { PageContainTable } from '../../../../components/PageContainTable'
export default defineComponent({
  components: {
    PageSearch,
    PageContainTable
  },
  setup() {
    return {
      formOptions,
      containTableConfig
    }
  }
})
</script>
复制代码

表格1.gif 下面再举一个在页面中传入插槽的定制的页面。

    // 定义需要展示哪些数据
    const propList = [
      { prop: 'name', label: '商品名称', minWidth: '80' },
      { prop: 'oldPrice', label: '原价格', minWidth: '80', slotName: 'oldPrice' },
      { prop: 'newPrice', label: '现价格', minWidth: '80', slotName: 'newPrice' },
      { prop: 'imgUrl', label: '商品图片', minWidth: '100', slotName: 'image' },
      { prop: 'status', label: '状态', minWidth: '100', slotName: 'status' },
      {
        prop: 'createAt',
        label: '创建时间',
        minWidth: '250',
        slotName: 'createAt'
      },
      {
        prop: 'updateAt',
        label: '更新时间',
        minWidth: '250',
        slotName: 'updateAt'
      },
      // 操作列
      {
        label: '操作',
        slotName: 'operateColumn',
        width: '150'
      }
    ]

    const containTableConfig = {
      listTitle: '商品列表',
      propList,
      isNumberColumn: true,
      isSelectColumn: true
    }

    export default containTableConfig
复制代码

页面组件

<template>
  <div class="goods">
    <page-search :formOptions="formOptions"></page-search>
    <page-contain-table
      :containTableConfig="containTableConfig"
      pageName="goods"
    >
      <!-- 对图片的插槽处理 -->
      <template #image="scope">
        <el-image
          style="width: 60px; height: 60px"
          :src="scope.row.imgUrl"
          :preview-src-list="[scope.row.imgUrl]"
          hide-on-click-modal
        >
        </el-image>
      </template>
      <!-- 对价格进行处理 -->
      <template #oldPrice="scope"> ¥{{ scope.row.oldPrice }} </template>
      <template #newPrice="scope"> ¥{{ scope.row.newPrice }} </template>
    </page-contain-table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import formOptions from './config/formConfig'
import containTableConfig from './config/containTableConfig'
import { PageSearch } from '../../../../components/PageSearch'
import { PageContainTable } from '../../../../components/PageContainTable'
export default defineComponent({
  name: 'goods',
  components: {
    PageSearch,
    PageContainTable
  },
  setup() {
    return {
      formOptions,
      containTableConfig
    }
  }
})
</script>
复制代码

表格2.gif

猜你喜欢

转载自juejin.im/post/7032109872577511454