likes
comments
collection
share

一个绝对优雅的筛选表格数据组件

作者站长头像
站长
· 阅读数 12

🙌大家好,今天给大家带来的是一个 Vue3 中的筛选表格数据组件。

在线预览,可以改变窗口大小看看效果。

前言

现在几乎所有的后台管理系统都包含有表格,用来展示数据。同时也会包含有用来筛选表格数据的条件查询部分,这一部分基本上由输入框、选择器、查询按钮和重置条件查询按钮组成。不同公司、不同系统这一部分布局可能互不相同,现在打开你的后台管理系统看看是什么样子的!

是像 Ant Design Pro 这样? 一个绝对优雅的筛选表格数据组件

还是像 TDesign Starter 这样? 一个绝对优雅的筛选表格数据组件

或是像 Arco Design Pro 这样? 一个绝对优雅的筛选表格数据组件

不同的表格条件筛选器布局可能很多,上面列举了几种不同的布局形式,大家可以选一选自己最喜欢哪种布局。 就我个人而言的话,比较喜欢 Arco Design Pro 中的布局,将输入区域和按钮区域区分的比较明显,并且空间利用率也比较高,但是它还是不具备一些我想要的功能。

使用场景分析

其实不同页面表格中的筛选项个数不同,整个页面的布局应该也需要有所调整。这一部分的布局应该在不同的场景下都应该有比较美观和协调,下面几个是我觉得需要具备的功能:

  • 当筛选项只有一行时应该将按钮固定在同一行,以此来节约空间;
  • 当筛选项有多行时才应该把两个按钮上下进行叠放;
  • 当筛选项过多时提供展开、收起功能;
  • 需要根据屏幕尺寸来动态变更每一行的筛选项个数;

弄清楚需求之后就可以开始进行组件封装了,在这里我使用的是 Vue3 + Arco Design 技术栈来开发,如果使用其他的组件库也可以继续往下阅读,里面核心功能采用的思路是通用的。

创建组件以及基本布局

创建 TableSearchWrapper.vue 文件,并构建基本布局。

<template>
  <a-row>
    <a-col :flex="1">
        <!-- 此处就是用来显示输入框、选择器这类组件的地方 -->
    </a-col>
    <!-- 此处的 198px 是根据按钮和分割线的再加上按钮在同一排的间隔计算而出,可以自由修改 -->
    <a-col flex="198px" style="display: flex">
      <!-- 32px 为一行的高度 -->
      <a-divider direction="vertical" style="height: 32px" />
      <!-- 放置按钮区域 -->
      <div 
        style="
          flex: 1;
          display: flex;
          justify-content: space-between;
          flex-wrap: wrap;
        "
      >
        <a-button type="primary">
          <template #icon>
            <icon-search />
          </template>
          查询
        </a-button>
        <a-button>
          <template #icon>
            <icon-refresh />
          </template>
          重置
        </a-button>
      </div>
    </a-col>
  </a-row>
</template>

<script setup>
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon';
</script>

这样就是基本的左侧条件输入区域和右侧按钮区域。 一个绝对优雅的筛选表格数据组件

条件输入区域渲染

因为不同页面、表格中的筛选条件不一致,因此需要通过插槽的方式来渲染,使用时只需要将每个输入控件作为组件内部的元素并指定一个具体的插槽名称即可。 此处我们在组件封装时就需要获取到所有的插槽,并遍历渲染出来。

获取所有插槽

<script setup>
import { computed, toRaw, useSlots } from 'vue'

// 获取所有插槽名称
const slots = useSlots()
const allSlotsName = computed(() => Object.keys(toRaw(slots)))
</script>

此处需要注意的是,如果使用此组件时但是内部的元素没有指定插槽名称,那这些元素当成默认插槽处理,并位于allSlotsName数组中的最后一位,因此当渲染插槽时,默认插槽在最后渲染。

渲染所有插槽

<template>
  <a-col :flex="1">
    <!-- 此处就是用来显示输入框、选择器这类组件的地方 -->
    <a-grid :col-gap="16" :row-gap="0" :cols="cols">
      <!-- 遍历插槽 -->
      <slot v-for="item in allSlotsName" :key="item" :name="item"></slot>
    </a-grid>
  </a-col>
</template>

<script setup>
const cols = { xs: 1, sm: 2, md: 3, lg: 4, xl: 5, xxl: 6 };
</script>

这里我直接使用的是 Arco Design 中的 a-grid 组件,通过传递cols它能很方便的实现的响应式布局,可以通过配置项实现在不同的屏幕尺寸中一行显示几个输入控件。 一个绝对优雅的筛选表格数据组件 预览效果如下: 一个绝对优雅的筛选表格数据组件

判断输入控件占据几行

现在来实现“当筛选项有多行时把两个按钮上下进行叠放”这个需求,首先我们肯定去需要监听页面尺寸变化,以此来判断当前页面尺寸处于 Bootstrap 响应式设计 的哪个区间。

看到这有人可能想到监听页面尺寸变化再去计算当前页面处于哪个区间,以此去实现,当然这样是可以实现的,但是有一种更简单的方法,无需上面复杂的逻辑。直接使用 Window.matchMedia API 即可。

window.matchMedia(mediaQueryString)通过传递一个被用于媒体查询解析的字符串,返回一个 MediaQueryList对象,对象中包含matches属性,满足条件时为true

下面就是我实现这一功能的核心代码。

<script setup>
import { computed, onMounted, reactive, onUnmounted } from 'vue';

// 保存媒体查询条件
const responsiveMap = {
  xs: '(max-width: 575px)',
  sm: '(min-width: 576px)',
  md: '(min-width: 768px)',
  lg: '(min-width: 992px)',
  xl: '(min-width: 1200px)',
  xxl: '(min-width: 1600px)'
};
// 保存当前页面尺寸处于哪个区间
const screens = reactive({
  xs: true,
  sm: true,
  md: true,
  lg: true,
  xl: true,
  xxl: true
});
// 页面尺寸从大到小
const colArr = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
// 计算当前页面尺寸下一行中可以有几个元素
const resultCol = computed(() => {
  let res = 1;
  for (let i = 0; i < colArr.length; i++) {
    const breakpoint = colArr[i];
    if (screens[breakpoint] || breakpoint === 'xs') {
      res = cols[breakpoint];
      break;
    }
  }
  return res;
});

const matchHandlers = {};
// 注册监听器,当页面大小改变时更新 screens
onMounted(() => {
  // 遍历 responsiveMap
  Object.keys(responsiveMap).forEach(screen => {
    const matchMediaQuery = responsiveMap[screen];
    if (!matchMediaQuery) return;
    // 事件监听器的回调函数
    const listener = ({ matches }) => {
      screens[screen] = matches;
    };
    // 媒体查询结果
    const mql = window.matchMedia(matchMediaQuery);
    // 给 MediaQueryList 对象增加 change 事件监听器
    if (mql.addEventListener) {
      mql.addEventListener('change', listener);
    } else {
      mql.addListener(listener);
    }
    // 保存不同媒体查询的结果
    matchHandlers[matchMediaQuery] = {
      mql,
      listener
    };
    // 手动执行回调函数,更新组件挂载时的页面尺寸信息
    listener(mql);
  });
});
// 移除监听器
onUnmounted(() => {
  Object.keys(responsiveMap).forEach(screen => {
    const matchMediaQuery = responsiveMap[screen];
    if (!matchMediaQuery) return;
    const handler = matchHandlers[matchMediaQuery];
    if (handler && handler.mql && handler.listener) {
      if (handler.mql.removeEventListener) {
        handler.mql.removeEventListener('change', handler.listener);
      } else {
        handler.mql.removeListener(handler.listener);
      }
    }
  });
});
</script>

现在就能通过resultColallSlotsName.length进行比较,来判断左侧条件筛选区域占据几行,以此在控制右侧按钮如何摆放。为了方便使用创建一个multipleRows计算属性,用于表示条件筛选区域是否占据多行。

<script setup>
import {  computed } from 'vue';

const multipleRows = computed(() => {
  return allSlotsName.value.length > resultCol.value;
});
</script>

接下来通过multipleRows去改变页面样式,以此来实现“当筛选项有多行时把两个按钮上下进行叠放

<template>
  <a-row>
    <a-col :flex="1">
    </a-col>
+     <a-col :flex="multipleRows ? '108px' : '198px'" style="display: flex">
+     <a-divider direction="vertical" :style="{ height: multipleRows ? '84px' : '32px' }" />
      <div 
        style="
          flex: 1;
          display: flex;
          justify-content: space-between;
          flex-wrap: wrap;
        "
      >
        <a-button
          type="primary"
+         :style="{
+           marginBottom: multipleRows ? '20px' : '0px'
+         }"
        >
          <template #icon>
            <icon-search />
          </template>
          查询
        </a-button>
        <a-button>
          <template #icon>
            <icon-refresh />
          </template>
          重置
        </a-button>
      </div>
    </a-col>
  </a-row>
</template>

上面样式有修改的地方已经标记出来,具体样式可以根据自己需求去改。 预览效果如下: 一个绝对优雅的筛选表格数据组件

集成 Form 组件以及绑定按钮点击事件

<template>
  <a-row>
    <a-col :flex="1">
      <!-- 此处就是用来显示输入框、选择器这类组件的地方 -->
      <a-form :model="props.form" :auto-label-width="true">
        <a-grid :col-gap="16" :row-gap="0" :cols="cols">
          <slot v-for="item in allSlotsName" :key="item" :name="item"></slot>
        </a-grid>
      </a-form>
    </a-col>
    <a-col flex="198px" style="display: flex">
      <a-divider direction="vertical" />
      <!-- 放置按钮区域 -->
      <div
        style="
          flex: 1;
          display: flex;
          justify-content: space-between;
          flex-wrap: wrap;
        "
      >
        <a-button
          @click="onSearchQuery"
          type="primary"
        >
          <template #icon>
            <icon-search />
          </template>
          查询
        </a-button>
        <a-button @click="onResetQuery">
          <template #icon>
            <icon-refresh />
          </template>
          重置
        </a-button>
      </div>
    </a-col>
  </a-row>
</template>

<script setup>
const props = defineProps({
  // 表单数据对象
  form: { type: Object, required: true },
  onSearchQuery: { type: Function, default: () => {} },
  onResetQuery: { type: Function, default: () => {} }
});
</script>

使用组件示例

<template>
  <TableSearchWrapper :form="form">
    <template v-slot:item1>
      <a-form-item field="field1" label="筛选项1">
        <a-input v-model="form.field1" />
      </a-form-item>
    </template>
    <template v-slot:item2>
      <a-form-item field="field2" label="筛选项2">
        <a-input v-model="form.field2" />
      </a-form-item>
    </template>
    <template v-slot:item3>
      <a-form-item field="field3" label="筛选项3">
        <a-input v-model="form.field3" />
      </a-form-item>
    </template>
    <template v-slot:item4>
      <a-form-item field="field4" label="筛选项4">
        <a-input v-model="form.field4" />
      </a-form-item>
    </template>
    <template v-slot:item5>
      <a-form-item field="field5" label="筛选项5">
        <a-input v-model="form.field5" />
      </a-form-item>
    </template>
    <template v-slot:item6>
      <a-form-item field="field6" label="筛选项6">
        <a-input v-model="form.field6" />
      </a-form-item>
    </template>
  </TableSearchWrapper>
</template>

<script setup>
import { reactive } from 'vue';

const form = reactive({
  field1: '',
  field2: '',
  field3: '',
  field4: '',
  field5: '',
  field6: ''
});
</script>

折叠/展开功能

此功能直接使用 Arco Design 中<a-grid>组件自带的折叠展开功能即可,但是使用之前需要将遍历渲染插槽那一部分进行修改为:

<a-grid :col-gap="16" :row-gap="0" :cols="cols" :collapsed="collapsed">
  <a-grid-item v-for="item in allSlotsName" :key="item">
    <slot :name="item"></slot>
  </a-grid-item>
</a-grid>

然后直接控制<a-grid>组件上的collapsed属性即可。 如何其他组件库没有此功能,我们可以新建一个数组,用来存储需要渲染的插槽名称,点击折叠和展开时直接操作该数组即可。

一个绝对优雅的筛选表格数据组件

最后

到这组件就算封装完了,封装此组件的目的在于可以统一我们管理端页面风格,同时也能够适应不同页面尺寸以及不同筛选条件个数。即使各位和我使用的不是相同的技术栈,但是封装组件的思路和逻辑还是大同小异的。

此组件不算复杂,里面用window.matchMedia()来替代监听windowonsize事件实现监听页面大小变化,我觉得算是比较优雅的(毕竟我之前还不知道这个API😅)。

如果在这篇文章中有所收获,不妨给小弟一个“三连”吧!😉😉😉

所有代码都已上传至 GitHub,欢迎 Start⭐,要是有其他问题和需求也可随时指出!

共同成长,无限进步!!! 💪