Vue3基于element-plus二次封装搜索列表组件

背景

由于公司大量中后台项目列表搜索页面均为顶部为列表筛选表单、中部为列表、底部分页模式,在开发时会出现大量重复代码的cv操作,固对element进行二次封装搜索表单组件,以提升开发效率,方便代码阅读和维护;

该篇仅用作个人工作记录使用,如有不足之处希望各位大佬轻喷,感谢!

组件封装实现

实现思路

组件主要拆分为两个部分:1、顶部筛选栏部分;2、列表 + 分页部分;

这样拆分的好处是可以覆盖大多数列表页面的业务场景;如果有比较定制化的需求,可以将列表和筛选栏组件拆开分别引入使用,减少了组件的耦合性,增强了组件的可扩展性;

组件主要接收两个参数,一个为顶部筛选栏的配置项searchConfig,用于配置顶部筛选栏的表单项类型、绑定的key、筛选项文案等;第二个参数为proTableConfig,透传给封装的pro-table组件用于配置列表和分页器;同时组件接收beforeSearch方法在每次筛选前可以对绑定的参数进行二次处理,以便于列表接口请求;

ProSearchTable搜索表格封装
<template>
  <div class="search-table">
    <!-- 搜索栏start -->
    <pro-search
      v-if="searchTempList.length"
      :is-operate="false"
      v-bind="proSearchConfig"
      @search="handleSearch"
      @clear="handleClear"
    >
      <pro-search-item
        v-for="(searchItem, index) in searchTempList"
        :key="index"
      >
        <template #content>
          <search-form-item
            v-model="query[searchItem.key]"
            :search-item="searchItem"
            @update="handleUpdate"
          />
        </template>
      </pro-search-item>
    </pro-search>
    <!-- 搜索栏end -->

    <!-- 列表start -->
    <div class="card">
      <!-- 按钮插槽 -->
      <div v-if="showBtnContainer" class="tw-mb-16">
        <slot name="btn" />
      </div>
      <pro-table ref="proTableRef" v-bind="_proTableConfig" />
    </div>
    <!-- 列表end -->
  </div>
</template>

<script setup>
  import { ProTable, ProSearch, ProSearchItem } from '../../index'
  import SearchFormItem from './search-form-item.vue';
  import { computed, useSlots, reactive, ref } from 'vue';

  // proSearch默认配置项
  const defaultProSearchConfig = {
    isOperate: false
  };

  const props = defineProps({
    searchConfig: {
      /**
       * 搜索栏配置项
       * searchTempList:{ 搜索项模板列表
       *  label: 筛选项名称
       *  key:筛选项入参字段名
       *  type:筛选项类型
       *  elementConfig:element原生的props配置,透传给element
       * }
       * proSearchConfig:ProSearch配置项,透传给ProSearch
       */
      type: Object,
      required: true
    },
    proTableConfig: {
      /** ProTable配置项,透传给ProTable */
      type: Object,
      required: true
    },
    beforeSearch: {
      /** 
       * 在列表接口请求前的事件,事件会返回search栏对应绑定的query,
       * 可以在此方法中处理接口请求入参,改函数return的参数就是最后请求接口的参数;
       */
      type: Function,
      default: () => {}
    }
  });
  const slots = useSlots();
  const query = reactive({
    ...(props.proTableConfig.fetchParams || {})
  });
  const proTableRef = ref(null);

  const searchTempList = computed(() => {
    const list = props.searchConfig.searchTempList;
    if (Array.isArray(list)) {
      return list;
    } else {
      return [];
    }
  });

  // 是否展示btn插槽外的盒子
  const showBtnContainer = computed(() => {
    const slotKeys = Object.keys(slots || []);
    return slotKeys.some(key => key === 'btn');
  })

  // proSearch配置项
  const proSearchConfig = computed(() => {
    return Object.assign(defaultProSearchConfig, props.searchConfig.proSearchConfig || {});
  });

  const _proTableConfig = computed(() => {
    const proTableConfig = props.proTableConfig;
    return {
      ...proTableConfig,
      // 如果传入beforeSearch,此处需要执行beforeSearch处理后的参数作为proTable的fetchParams
      fetchParams: props.beforeSearch && proTableConfig.fetchParams ? 
        props.beforeSearch(deepCopy(proTableConfig.fetchParams)) : 
        proTableConfig.fetchParams
    }
  })

  // 列表接口请求
  const handleSearch = () => {
    const payload = getPayload();
    proTableRef.value.init({
      param: payload,
    });
  }

  // 获取请求参数
  const getPayload = () => {
    let payload = {};
    for (const key in query) {
      const val = query[key];
      // 去掉非空参数
      val !== '' &&
        val !== undefined &&
        val !== null &&
        (payload[key] = val);
    }
    // 如果传入beforeSearch,此处需要执行beforeSearch处理后的参数作为接口请求入参
    return props.beforeSearch && props.beforeSearch(payload) || payload;
  };

  // 清空筛选
  const handleClear = () => {
    for (const key in query) {
      const fetchParams = props.proTableConfig.fetchParams;
      // 此处如果有传入fetchParams,筛选之重置为fetchParams传入的默认值
      const initValue = fetchParams ? fetchParams[key] : '';
      query[key] = initValue || '';
    }
  }

  // 筛选项改变回调
  const handleUpdate = (val, searchInfo) => {
    const key = searchInfo.key;
    query[key] = val;
  }

  const deepCopy = v => {
    if (!v) return null;
    return JSON.parse(JSON.stringify(v))
  };

  (function init() {
    if (Array.isArray(searchTempList.value)) {
      // searchTempList根据初始化query
      searchTempList.value.forEach(e => { 
        const key = e.key;
        const fetchParams = props.proTableConfig.fetchParams;
        // 传入的fetchParams为筛选项的初始默认值
        const initValue = fetchParams ? fetchParams[key] : '';
        query[key] = initValue || '';
      });
    }
  })()

  defineExpose({
    getPayload,
    handleSearch,
    handleClear
  });
</script>

<style scoped>
  .card {
    margin-top: 16px;
    background: #fff;
    padding: 16px;
    border-radius: 8px;
    box-sizing: border-box;
  }
</style>
顶部筛选栏封装

组件中的pro-search和pro-search-item为左边筛选标签、右边表单项布局组件附带查询和筛选按钮,比较简单固不做展示,筛选栏部分筛选表单项组件(search-form-item)封装如下:

search-form-item
<template>
  <el-input
    v-if="type === 'input'"
    v-model="bindValue"
    :placeholder="placeholder"
    clearable
    v-bind="elementConfig"
    @input="handleChange"
  />
  <el-select
    v-else-if="type === 'select'"
    v-model="bindValue"
    class="tw-w-full"
    v-bind="elementConfig"
    clearable
    :placeholder="placeholder"
    @change="handleChange"
  >
    <el-option
      v-for="item in elementConfig.options || []"
      :key="item[elementConfig.valueName || 'value']"
      :label="item[elementConfig.labelName || 'label']"
      :value="item[elementConfig.valueName || 'value']"
    />
  </el-select>
  <ProDatePicker
    v-else-if="type === 'date'"
    type="daterange"
    :editable="false"
    v-model="bindValue"
    :start-placeholder="placeholder"
    end-placeholder="结束时间"
    value-format="YYYY-MM-DD"
    format="YYYY/MM/DD"
    @change="handleChange"
    style="width: 100%;"
    v-bind="elementConfig"
  />
  <el-cascader
    v-else-if="type === 'cascader'"
    v-model="bindValue"
    clearable
    :placeholder="placeholder"
    style="width: 100%;"
    v-bind="elementConfig"
    @change="handleChange"
  />
</template>

<script setup>
  import { computed, watch, ref } from 'vue';
  import { ProDatePicker } from '../../index';

  const props = defineProps({
    /**
     * 配置项
     * value:绑定值
     * type:对应的表单类型
     *  input:输入框
     *  select:下拉选择框
     *  dater:时间段筛选框
     *  cascader:级联选择器
     * label:placeholder类型文案
     * elementConfig:element原生的props配置,透传给element
     *                (select比较特殊,valueName为绑定的值的key,
     *                 labelName为绑定展示文案的key,options为下拉选先数据源)
     */
    searchItem: {
      type: Object,
      required: true,
    },
    modelValue: {
      type: String,
      required: true,
    }
  });
  const emits = defineEmits('update');

  const bindValue = ref(props.modelValue); // 表单项绑定值

  const type = computed(() => props.searchItem.type);

  const placeholder = computed(() => {
    const label = props.searchItem.label;
    switch (type.value) {
      case 'select':
      case 'cascader':
        return `请选择${label}`;
      case 'date':
        return `${label}开始时间`;
      default:
        return `请输入${label}`;
    }
  })

  const elementConfig = computed(() => props.searchItem.elementConfig || {});

  watch(() => props.modelValue, val => { // 保证绑定的bindValue和传入的value一致
    if (props.searchItem.type === 'date') {
      // 时间类型清空绑定值(bindValue)重置为空数组
      !val && (bindValue.value = []);
    } else {
      bindValue.value = val;
    }
  });

  const handleChange = (val) => {
    val = val === null ? '' : val; // props不可以传null回来,会有vue警告,针对时间段选择器
    emits('update', val, props.searchItem);
  }
</script>

<style>

</style>
列表+分页组件封装
pro-table

*注意:透传属性/事件都包在了_options里面一并传给了el-table

props.options为透传给element的属性配置;

列表渲染支持插槽、render函数(jsx语法也支持)、h函数等方式进行渲染;

<template>
  <div class="component-wrapper">
    <el-config-provider :locale="local">
      <el-table
        style="width: 100%"
        ref="tableRef"
        :data="fetchData"
        v-bind="_options"
      >
        <template v-for="(col, index) in columns" :key="index">
          <!-- 渲染插槽 START -->
          <TableColumn :col="col" @command="handleAction">
            <template
              v-for="(slot, index) in Object.keys($slots)"
              #[slot]="scope"
            >
              <slot :name="slot" v-bind="scope"></slot>
            </template>
          </TableColumn>
          <!-- 渲染插槽 END -->
        </template>
        <!-- 自定义空状态-->
        <template #empty>
          <slot name="empty">
            <div style="text-align: center">
              <el-image
                class="empty-img"
                src="https://static.wxb.com.cn/frontEnd/images/idea-middle-platform/empty.png"
              ></el-image>
              <div class="empty-text">暂无数据</div>
            </div>
          </slot>
        </template>
      </el-table>
    </el-config-provider>

    <!-- 分页器 -->
    <div
      v-if="_options.showPagination && _paginationConfig.total > 0"
      class="mt20 page-wrapper"
    >
      <slot name="page"></slot>
      <el-config-provider :locale="local">
        <el-pagination
          v-bind="_paginationConfig"
          @size-change="pageSizeChange"
          @current-change="currentPageChange"
        />
      </el-config-provider>
    </div>
  </div>
</template>
<script setup name="ProTable">
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { computed, ref, reactive, onMounted, watch, useAttrs } from "vue";
import TableColumn from "./table-column.vue";

const local = ref(zhCn);
const tableRef = ref(null);
const props = defineProps({
  fetchApi: {
    type: Function,
  },
  fetchParams: {
    type: Object,
  },
  tableData: {
    type: Array,
    default: [],
  },
  columns: {
    type: Array,
    default: [],
    required: true,
  },
  options: {
    type: Object,
    default: {},
  },
  selectionChange: {
    type: Function,
  },
  noEmpty: {
    type: Boolean,
    default: false,
  },
});
const fetchData = ref([]);
const attrs = useAttrs();

//  监听tableData数据变化
watch(
  () => props.tableData,
  (newValues) => {
    if (props.tableData) {
      fetchData.value = newValues;
    }
  },
  { deep: true, immediate: true }
);

// table参数
const _options = computed(() => {
  const options = {
    border: false, // 是否带有纵向边框
    maxHeight: "calc(100vh - 195px)",
    headerCellStyle: {
      background: "#F8FAFB", //表头背景
      color: "#2C2C34",
      border: 0,
      borderRadius: "4px",
      height: "56px",
    },
    cellStyle: {
      borderLeft: 0,
      borderRight: 0,
      borderBottom: "1px solid #F3F7F8",
      height: "80px",
    },
    paginationConfig: {}, //分页配置
    rowStyle: () => "cursor:pointer", //设置行样式
    showPagination: true, // 是否显示分页
  };
  // 此处把其他绑定事件也透传给el-table
  return Object.assign(options, props.options, attrs);
});

// 分页器参数
const config = {
  total: 0, // 总条数
  currentPage: 1, // 当前页
  pageSize: 10, // 分页数量
  pageSizes: [10, 50, 100], //每页显示个数选择器的选项设置
  layout: "total, sizes, prev, pager, next,jumper", // 组件布局,子组件名用逗号分隔
};
const _paginationConfig = reactive(
  Object.assign(config, _options.value.paginationConfig)
);
let searchParams = reactive({});

// 切换pageSize
const pageSizeChange = (pageSize) => {
  _paginationConfig.pageSize = pageSize;
  _paginationConfig.currentPage = 1;
  init(searchParams, "1");
};

// 切换currenPage
const currentPageChange = (pageNum) => {
  _paginationConfig.currentPage = pageNum;
  init(searchParams, "1");
};

// 初始化
const init = (params = {}, type) => {
  if (type !== "1") {
    // 其他查询重置页码
    _paginationConfig.currentPage = 1;
  }
  const initParams = Object.assign(
    {
      pageNum: _paginationConfig.currentPage,
      pageSize: _paginationConfig.pageSize,
    },
    { ...params }
  );
  searchParams = params; // 记录入参用于翻页或跳页
  props.fetchApi &&
    props
      .fetchApi(initParams)
      .then((res) => {
        if (res && res.data) {
          const {
            data: { total: _total, list },
          } = res;
          fetchData.value = list || [];
          _paginationConfig.total = _total || 0;
          emits("on-fetch-success", res.data);
        } else {
          fetchData.value = [];
          _paginationConfig.total = 0;
        }
      })
      .catch((e) => {
        console.log("表格fetchApi异常:", e);
      });
};

onMounted(() => {
  if (props.fetchApi) {
    // 初始化需要除了分页相关的其它参数,需要特殊梳理
    if (props.fetchParams) {
      init({ param: props.fetchParams });
    } else {
      init();
    }
  }
});

defineExpose({
  init,
  tableRef,
  fetchData,
});
</script>
table-column
<template>
  <!-- 如果有配置多级表头的数据,则递归该组件 -->
  <template v-if="col.children?.length">
    <el-table-column :label="col.label" :width="col.width" :align="col.align">
      <TableColumn v-for="item in col.children" :col="item" :key="item.prop">
        <template v-for="slot in Object.keys($slots)" #[slot]="scope">
          <slot :name="slot" v-bind="scope" />
        </template>
      </TableColumn>
      <template #header="{ column, $index }">
        <component
          v-if="col.headerRender"
          :is="col.headerRender"
          :column="column"
          :index="$index"
        />
        <slot
          v-else-if="col.headerSlot"
          :name="col.headerSlot"
          :column="column"
          :index="$index"
        ></slot>
        <span v-else>{{ column.label }}</span>
      </template>
    </el-table-column>
  </template>

  <!---复选框, 序号 (START)-->
  <el-table-column
    v-if="
      col.type === 'index' || col.type === 'selection' || col.type === 'expand'
    "
    :index="index"
    v-bind="col"
  >
    <!-- 当type等于expand时, 配置通过h函数渲染-->
    <template #default="{ row, $index }">
      <!-- render函数 (START) : 使用内置的component组件可以支持h函数渲染-->
      <component
        v-if="col.render"
        :is="col.render"
        :row="row"
        :index="$index"
      />
      <!-- render函数 (END) -->
      <!-- 自定义slot (START) -->
      <slot
        v-else-if="col.slot"
        :name="col.slot"
        :row="row"
        :index="$index"
      ></slot>
      <!-- 自定义slot (END) -->
    </template>
  </el-table-column>
  <!---复选框, 序号 (END)-->

  <!-- 其他正常列 -->
  <el-table-column v-else v-bind="col">
    <!-- 自定义表头 -->
    <template #header="{ column, $index }">
      <!-- render渲染 -->
      <component
        v-if="col.headerRender"
        :is="col.headerRender"
        :column="column"
        :index="$index"
      />
      <!-- 插槽渲染 -->
      <slot
        v-else-if="col.headerSlot"
        :name="col.headerSlot"
        :column="column"
        :index="$index"
      ></slot>
      <span v-else>{{ column.label }}</span>
    </template>
    <template #default="{ row, $index }">
      <!-- render函数 (START) 使用内置的component组件可以支持h函数渲染和txs语法-->
      <component
        v-else-if="col.render"
        :is="col.render"
        :row="row"
        :index="$index"
      />
      <!-- render函数 (END) -->
      <!-- 自定义slot (START) -->
      <slot v-else-if="col.slot" :name="col.slot" :row="row" :index="$index">
        <!-- <div>插槽自定义{{row.sex}}</div> -->
      </slot>
      <!-- 自定义slot (END) -->
      <!-- 默认渲染 (START) -->
      <span v-else>{{
        (row[col.prop] ?? "") === "" ? "-" : row[col.prop]
      }}</span>
      <!-- 默认渲染 (END) -->
    </template>
  </el-table-column>
</template>

<script setup>
defineProps({
  col: {
    type: Object,
    required: true,
  },
});
</script>

使用效果

效果示例代码

<template>
  <div class="page-content">
    <div class="examples-box">
        <ProSearchTable
          ref="proSearchTableRef"
          :search-config="searchConfig"
          :pro-table-config="proTableConfig"
          :before-search="handleBeforeSrc"
        >
          <template #btn>
            <ProButton type="primary" style="margin-bottom: 20px;">btn插槽</ProButton>
          </template>
        </ProSearchTable>
        <div style="margin-top: 100px;">
          <ProButton type="primary" @click="handleSearch">手动搜索</ProButton>
          <ProButton type="primary" @click="handleClear">手动清除</ProButton>
          <ProButton type="primary" @click="getPayload">获取请求参数</ProButton>
        </div>
    </div>
  </div>
</template>
<script setup lang="jsx">
import { ProSearchTable, ProButton } from "../../../../packages";
import { ref, computed } from "vue";

// 模拟接口请求
const getTableData = (payload) => {
  console.log('payload==>', payload);
  return new Promise(
    (resolve) => {
    setTimeout(() => {
      resolve({
        code: 10000,
        success: true,
        data: {
          total: 1,
          list: [{
            date: '2024-09-19',
            name: '小明',
            sex: '男',
            address: '浙江杭州',
        }]
        }
      })
    }, 500);
  })
}
  const sexOptions = ref([]);
  const areaOptions = ref([]);
  const proSearchTableRef = ref(null);

  const searchConfig = computed(() => ({
    searchTempList: [
      { type: 'input', label: '姓名', pleaceholder: '请输入', key: 'name' },
      { type: 'date', label: '日期', key: 'dateArr', elementConfig: {
        'type': 'datetimerange',
        'value-format': 'YYYY-MM-DD HH:mm:ss',
        'format': 'YYYY/MM/DD HH:mm:ss'
      } },
      { type: 'select', label: '性别', pleaceholder: '请输入', key: 'sex',
        elementConfig: {
          valueName: 'itemId',
          labelName: 'itemName',
          options: sexOptions.value,
        }
      },
      { type: 'cascader', label: '地区', key: 'area',
        elementConfig: {
          options: areaOptions.value,
          props: { value: 'code', label: 'name', children: 'child', checkStrictly: true }
        },
      },
    ],
  }));

  const proTableConfig = computed(() => ({
    fetchApi: getTableData,
    /** 此处传入的fetchParams也就是筛选栏的初始值,点击重置按钮也会重置为这里传入的值 */
    fetchParams: {
      initParams: '01',
      name: '111',
      dateArr: [ "2024-05-17 00:00:00", "2024-06-19 23:59:59"]
    },
    columns: [
      { prop: 'name', label: '姓名' },
      { prop: 'sex', label: '性别', minWidth: '150px' },
      { prop: 'address', label: '地址', minWidth: '180px' },
      { prop: 'date', label: '日期', minWidth: '180px', render: ({ row }) => regionRender(row) },
    ],
  }));

  const regionRender = (row) => <span style={ {color: 'red'} }>{ row.date }</span>

  const handleBeforeSrc = payload => {
    console.log('before-search-payload', payload);
    return {
      ...payload,
      addKey: '2'
    }
  }

  const handleSearch = () => {
    proSearchTableRef.value.handleSearch();
  }
  const handleClear = () => {
    proSearchTableRef.value.handleClear();
  }
  const getPayload = () => {
    const payload = proSearchTableRef.value.getPayload();
    window.alert(`当前请求参数为:${JSON.stringify(payload)}`);
  }

</script>

搜索表单 Attributes

ProSearchTable
属性类型默认值描述

searchConfig

Object

--

搜索栏的配置项,详情见下表searchConfig配置表;

proTableConfig

Object

--

pro-table配置项,透传给pro-table

* 注意:proTableConfig传入的fetchParams也是筛选项的初始值 *

beforeSearch

Function

--

在列表接口请求前的事件,事件会返回search栏对应绑定的query,可以在此方法中处理接口请求入参,改函数return的参数就是最后请求接口的参数;

ProSearchTable事件
事件说明

handleSearch

触发列表接口请求;

handleClear

清空筛选栏;

getPayload

获取当前列表请求参数;
searchConfig配置
属性类型默认值描述

searchTempList

Array

--

搜索栏配置项,循环渲染出每一项搜索的form;详情见下表searchTempList配置;

proSearchConfig

Object

{
    isOperate: false
  }

ProSearch配置项,透传给ProSearch;

searchTempList配置
属性类型默认值描述

type

String--

对应的表单类型

     *  input:输入框
     *  select:下拉选择框
     *  date:时间筛选框
     *  cascader:级联选择器

label

String--对应渲染的placeholder类型文案

elementConfig

Object--

element原生的props配置,透传给element,select比较特殊,valueName为绑定的值的key,labelName为绑定展示文案的key,options为下拉选先数据源

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue2中,对于element-ui组件二次封装,可以按照以下步骤进行: 1. 需求分析:明确需要封装element-ui组件,以及需要添加的功能和配置项。 2. 创建父组件:编写父组件的template和script代码,其中template中调用封装组件,script中定义需要传递给封装组件的props属性。 3. 创建封装组件:编写封装组件的template和script代码。在template中使用element-ui组件,并根据需要进行样式和布局的调整。在script中定义props属性,接收父组件传递的值,并监听element-ui组件的事件,触发update事件给父组件。 4. 通过临时变量传递值:由于父组件传递给封装组件的props不能直接作为封装组件的v-model属性传递给element-ui组件,所以需要在封装组件中定义一个临时变量来存储值,并将该变量与element-ui组件进行绑定。 5. 完成打通:在封装组件中监听中间件,接收到element-ui组件的update事件后,再将该事件传递给父组件。 总结来说,Vue2中对于element-ui组件二次封装,需要创建父组件封装组件,通过props属性传递值,并在封装组件中监听element-ui组件的事件并触发update事件给父组件。同时,需要使用临时变量来传递值给element-ui组件。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Vue3+ts+element-plus 组件二次封装-- 页脚分页el-pagination的二次封装](https://blog.csdn.net/cs492934056/article/details/128096257)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值