问题描述

teambition软件是企业办公协同软件,相信部分朋友的公司应该用过这款软件。里面的筛选功能挺有意思,本篇文章,就是仿写其功能。我们先看一下最终做出来的效果图
image

大致的功能效果有如下

  • 需求一:常用筛选条件放在上面直接看到,不常用筛选条件放在添加筛选条件里面
  • 需求二:筛选的方式有输入框筛选、下拉框筛选、时间选择器筛选等
  • 需求三:如果觉得常用筛选条件比较多的话,可以鼠标移入点击删除,使之进入不常用的筛选条件里
  • 需求四:也可以从不常用的筛选条件里面点击对应筛选条件使之“蹦到”常用筛选条件里
  • 需求五:点击重置使之恢复到初试的筛选条件
  • 需求六:用户若是没输入内容点击确认按钮,就提示用户要输入筛选条件

思路分析

  • 对于需求一和需求二,我们首先要搞两个全屏幕弹框,然后在data中定义两个数组,一个是放常用条件的数组,另外一个是放不常用条件的数组,常用条件v-for到第一个弹框里面,不常用条件v-for到第二个弹框里面。数组里面的每一项都要配置好对应内容,比如要有筛选字段名字,比如姓名、年龄什么的。有了筛选筛选字段名字以后,还有有一个类型type,在html中我们要写三个类型的组件、比如input输入框组件,select组件,时间选择器组件。使用根据type类型通过v-show显示对应字段,比如input的type为1,select的type为2,时间选择器的type为3。是哪个type,就显示哪个组件。

对应两个数组如下:

topData: [ // 配置常用的筛选项
        {
          wordTitle: "姓名",
          type: 1, // 1 为input 2为select 3为DatePicker
          content: "", // content为输入框绑定的输入数据
          options: [], // options为所有的下拉框内容,可以发请求拿到存进来,这里是模拟
          optionArr: [], // optionArr为选中的下拉框内容
          timeArr: [], // timeArr为日期选择区间
        },
        {
          wordTitle: "年龄",
          type: 1,
          content: "",
          options: [],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "授课班级",
          type: 2,
          content: "",
          options: [ // 发请求获取下拉框选项
            {
              id: 1,
              value: "一班",
            },
            {
              id: 2,
              value: "二班",
            },
            {
              id: 3,
              value: "三班",
            },
          ],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "入职时间",
          type: 3, 
          content: "", 
          options: [], 
          optionArr: [], 
          timeArr: [], 
        },
      ],
      bottomData: [ // 配置不常用的筛选项
        {
          wordTitle: "工号",
          type: 1,
          content: "",
          options: [],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "性别",
          type: 2,
          content: "",
          options: [
            {
              id: 1,
              value: "男",
            },
            {
              id: 2,
              value: "女",
            },
          ],
          optionArr: [],
          timeArr: [],
        },
      ],

对应html代码如下:

                <div class="rightright">
                  <el-input
                    v-model.trim="item.content"
                    clearable
                    v-show="item.type == 1"
                    placeholder="请输入"
                    size="small"
                    :popper-append-to-body="false"
                  ></el-input>
                  <el-select
                    v-model="item.optionArr"
                    v-show="item.type == 2"
                    multiple
                    placeholder="请选择"
                  >
                    <el-option
                      v-for="whatItem in item.options"
                      :key="whatItem.id"
                      :label="whatItem.value"
                      :value="whatItem.id"
                      size="small"
                    >
                    </el-option>
                  </el-select>
                  <el-date-picker
                    v-model="item.timeArr"
                    v-show="item.type == 3"
                    type="daterange"
                    range-separator="至"
                    start-placeholder="开始日期"
                    end-placeholder="结束日期"
                    format="yyyy-MM-dd"
                    value-format="yyyy-MM-dd"
                  >
                  </el-date-picker>
                </div>
完整代码在最后,大家先顺着思路看哦
  • 对于需求三需求四,可描述为,删除上面的掉到下面。点击下面的蹦到上面。所以对应操作就是把上面数组某一项追加到下面数组,然后把上面数组的这一项删掉;把下面数组的某一项追加到上面数组,然后把这一行删掉。(注意还有一个索引)对应代码如下:
/* 点击某一项的删除小图标,把这一项添加到bottomData数组中
       然后把这一项从topData数组中删除掉(根据索引判别是哪一项) 
       最后删除一个就把索引置为初始索引 -1   */
    clickIcon(i) {
      this.bottomData.push(this.topData[i]);
      this.topData.splice(i, 1);
      this.whichIndex = -1;
    },
    // 点击底部的项的时候,通过事件对象,看看点击的是底部的哪一项
    // 然后把对应的那一项追加到topData中用于展示,同时把bottom数组
    // 中的哪一项进行删除
    clickBottomItem(event) {
      this.bottomData.forEach((item, index) => {
        if (item.wordTitle == event.target.innerText) {
          this.topData.push(item);
          this.bottomData.splice(index, 1);
        }
      });
    },
  • 对于需求五需求六就简单了,对应代码如下,完整代码注释中已经写好了

完整代码

<template>
  <div id="app">
    <div class="filterBtn">
      <el-button type="primary" size="small" @click="filterMaskOne = true">
        数据筛选<i class="el-icon-s-operation el-icon--right"></i>
      </el-button>
      <transition name="fade">
        <div
          class="filterMaskOne"
          v-show="filterMaskOne"
          @click="filterMaskOne = false"
        >
          <div class="filterMaskOneContent" @click.stop>
            <div class="filterHeader">
              <span>数据筛选</span>
            </div>
            <div class="filterBody">
              <div class="outPrompt" v-show="topData.length == 0">
                暂无筛选条件,请添加筛选条件...
              </div>
              <div
                class="filterBodyCondition"
                v-for="(item, index) in topData"
                :key="index"
              >
                <div
                  class="leftleft"
                  @mouseenter="mouseEnterItem(index)"
                  @mouseleave="mouseLeaveItem(index)"
                >
                  <span
                    >{{ item.wordTitle }}:
                    <i
                      class="el-icon-error"
                      v-show="whichIndex == index"
                      @click="clickIcon(index)"
                    ></i>
                  </span>
                </div>
                <div class="rightright">
                  <el-input
                    v-model.trim="item.content"
                    clearable
                    v-show="item.type == 1"
                    placeholder="请输入"
                    size="small"
                    :popper-append-to-body="false"
                  ></el-input>
                  <el-select
                    v-model="item.optionArr"
                    v-show="item.type == 2"
                    multiple
                    placeholder="请选择"
                  >
                    <el-option
                      v-for="whatItem in item.options"
                      :key="whatItem.id"
                      :label="whatItem.value"
                      :value="whatItem.id"
                      size="small"
                    >
                    </el-option>
                  </el-select>
                  <el-date-picker
                    v-model="item.timeArr"
                    v-show="item.type == 3"
                    type="daterange"
                    range-separator="至"
                    start-placeholder="开始日期"
                    end-placeholder="结束日期"
                    format="yyyy-MM-dd"
                    value-format="yyyy-MM-dd"
                  >
                  </el-date-picker>
                </div>
              </div>
            </div>
            <div class="filterFooter">
              <div class="filterBtn">
                <el-button
                  type="text"
                  icon="el-icon-circle-plus-outline"
                  @click="filterMaskTwo = true"
                  >添加筛选条件</el-button
                >
                <transition name="fade">
                  <div
                    class="filterMaskTwo"
                    v-show="filterMaskTwo"
                    @click="filterMaskTwo = false"
                  >
                    <div class="filterMaskContentTwo" @click.stop>
                      <div class="innerPrompt" v-show="bottomData.length == 0">
                        暂无内容...
                      </div>
                      <div
                        class="contentTwoItem"
                        @click="clickBottomItem"
                        v-for="(item, index) in bottomData"
                        :key="index"
                      >
                        <div class="mingzi">
                          {{ item.wordTitle }}
                        </div>
                      </div>
                    </div>
                  </div>
                </transition>
              </div>
              <div class="resetAndConfirmBtns">
                <el-button size="small" @click="resetFilter">重置</el-button>
                <el-button type="primary" size="small" @click="confirmFilter"
                  >确认</el-button
                >
              </div>
            </div>
          </div>
        </div>
      </transition>
    </div>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    return {
      filterMaskOne: false, // 分别用于控制两个弹框的显示与隐藏
      filterMaskTwo: false,
      whichIndex: -1, // 用于记录点击的索引
      apiFilterArr:[], //存储用户填写的筛选内容
      topData: [ // 配置常用的筛选项
        {
          wordTitle: "姓名",
          type: 1, // 1 为input 2为select 3为DatePicker
          content: "", // content为输入框绑定的输入数据
          options: [], // options为所有的下拉框内容
          optionArr: [], // optionArr为选中的下拉框内容
          timeArr: [], // timeArr为日期选择区间
        },
        {
          wordTitle: "年龄",
          type: 1,
          content: "",
          options: [],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "授课班级",
          type: 2,
          content: "",
          options: [ // 发请求获取下拉框选项
            {
              id: 1,
              value: "一班",
            },
            {
              id: 2,
              value: "二班",
            },
            {
              id: 3,
              value: "三班",
            },
          ],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "入职时间",
          type: 3, 
          content: "", 
          options: [], 
          optionArr: [], 
          timeArr: [], 
        },
      ],
      bottomData: [ // 配置不常用的筛选项
        {
          wordTitle: "工号",
          type: 1,
          content: "",
          options: [],
          optionArr: [],
          timeArr: [],
        },
        {
          wordTitle: "性别",
          type: 2,
          content: "",
          options: [
            {
              id: 1,
              value: "男",
            },
            {
              id: 2,
              value: "女",
            },
          ],
          optionArr: [],
          timeArr: [],
        },
      ],
    };
  },
  mounted() {
    // 在初始化加载的时候,我们就把我们配置的常用和不常用的筛选项保存一份
    // 当用户点击重置按钮的时候,再取出来使其恢复到最初的筛选条件状态
    sessionStorage.setItem("topData",JSON.stringify(this.topData))
    sessionStorage.setItem("bottomData",JSON.stringify(this.bottomData))
  },
  methods: {
    //鼠标移入显示删除小图标
    mouseEnterItem(index) {
      this.whichIndex = index;
    },
    // 鼠标离开将索引回复到默认-1
    mouseLeaveItem() {
      this.whichIndex = -1;
    },
    /* 点击某一项的删除小图标,把这一项添加到bottomData数组中
       然后把这一项从topData数组中删除掉(根据索引判别是哪一项) 
       最后删除一个就把索引置为初始索引 -1   */
    clickIcon(i) {
      this.bottomData.push(this.topData[i]);
      this.topData.splice(i, 1);
      this.whichIndex = -1;
    },
    // 点击底部的项的时候,通过事件对象,看看点击的是底部的哪一项
    // 然后把对应的那一项追加到topData中用于展示,同时把bottom数组
    // 中的哪一项进行删除
    clickBottomItem(event) {
      this.bottomData.forEach((item, index) => {
        if (item.wordTitle == event.target.innerText) {
          this.topData.push(item);
          this.bottomData.splice(index, 1);
        }
      });
    },
    // 点击确认筛选
    async confirmFilter() {
      // 如果所有的输入框的content内容为空,且选中的下拉框数组为空,且时间选择器选中的数组为空
      // 就说明用户没有输入内容,那么我们就提示用户要输入内容以后再进行筛选
      let isEmpty = this.topData.every((item)=>{
        return (item.content == "") && (item.optionArr.length == 0) && (item.timeArr.length == 0)
      })
      if(isEmpty == true){
         this.$alert('请输入内容以后再进行筛选', '筛选提示', {
          confirmButtonText: '确定'
        });
      }else{
        // 收集参数发筛选请求,这里要分类型,把不为空的既有用户输入内容的
        // 存到存到数据筛选的数组中去,然后发请求给后端。
        this.topData.forEach((item)=>{
          if(item.type == 1){
            if(item.content != ""){
              let filterItem = {
                field:item.wordTitle,
                value:item.content
              }
              this.apiFilterArr.push(filterItem)
            }
          }else if(item.type == 2){
            if(item.optionArr.length > 0){
              let filterItem = {
                field:item.wordTitle,
                value:item.optionArr
              }
              this.apiFilterArr.push(filterItem)
            }
          }else if(item.type == 3){
            if(item.timeArr.length > 0){
              let filterItem = {
                field:item.wordTitle,
                value:item.timeArr
              }
              this.apiFilterArr.push(filterItem)
            }
          } 
        })
        // 把筛选的内容放到一个数组里面,传递给后端(当然不一定把参数放到数组里面)
        // 具体以怎样的形式传递给后端,可以具体商量
        console.log("带着筛选内容发请求",this.apiFilterArr);
      }
    },
    // 重置时,再把最初的配置筛选项取出来赋给对应的两个数组
    resetFilter() {
      this.topData = JSON.parse(sessionStorage.getItem("topData"))
      this.bottomData = JSON.parse(sessionStorage.getItem("bottomData"))
    },
  },
};
</script>
<style lang="less" scoped>
.filterBtn {
  width: 114px;
  height: 40px;
  .filterMaskOne {
    top: 0;
    left: 0;
    position: fixed;
    width: 100%;
    height: 100%;
    z-index: 999;
    background-color: rgba(0, 0, 0, 0.3);
    .filterMaskOneContent {
      position: absolute;
      top: 152px;
      right: 38px;
      width: 344px;
      height: 371px;
      background-color: #fff;
      box-shadow: 0px 0px 4px 3px rgba(194, 194, 194, 0.25);
      border-radius: 4px;
      .filterHeader {
        width: 344px;
        height: 48px;
        border-bottom: 1px solid #e9e9e9;
        span {
          display: inline-block;
          font-weight: 600;
          font-size: 16px;
          margin-left: 24px;
          margin-top: 16px;
        }
      }
      .filterBody {
        width: 344px;
        height: 275px;
        overflow-y: auto;
        overflow-x: hidden;
        box-sizing: border-box;
        padding: 12px 24px 0 24px;
        .outPrompt {
          color: #666;
        }
        .filterBodyCondition {
          width: 100%;
          min-height: 40px;
          display: flex;
          margin-bottom: 14px;
          .leftleft {
            width: 88px;
            height: 40px;
            display: flex;
            align-items: center;
            margin-right: 20px;
            span {
              position: relative;
              font-size: 14px;
              color: #333;
              i {
                color: #666;
                right: -8px;
                top: -8px;
                position: absolute;
                font-size: 15px;
                cursor: pointer;
              }
              i:hover {
                color: #5f95f7;
              }
            }
          }
          .rightright {
            width: calc(100% - 70px);
            height: 100%;
            /deep/ input::placeholder {
              color: rgba(0, 0, 0, 0.25);
              font-size: 13px;
            }
            /deep/ .el-input__inner {
              height: 40px;
              line-height: 40px;
            }
            /deep/ .el-select {
              .el-input--suffix {
                /deep/ input::placeholder {
                  color: rgba(0, 0, 0, 0.25);
                  font-size: 13px;
                }
                .el-input__inner {
                  border: none;
                }
                .el-input__inner:hover {
                  background: rgba(95, 149, 247, 0.05);
                }
              }
            }
            .el-date-editor {
              width: 100%;
              font-size: 12px;
            }
            .el-range-editor.el-input__inner {
              padding-left: 2px;
              padding-right: 0;
            }
            /deep/.el-range-input {
              font-size: 13px !important;
            }
            /deep/ .el-range-separator {
              padding: 0 !important;
              font-size: 12px !important;
              width: 8% !important;
              margin: 0;
            }
            /deep/ .el-range__close-icon {
              width: 16px;
            }
          }
        }
      }
      .filterFooter {
        width: 344px;
        height: 48px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        box-sizing: border-box;
        padding-left: 24px;
        padding-right: 12px;
        border-top: 1px solid #e9e9e9;
        .filterBtn {
          .filterMaskTwo {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.3);
            z-index: 1000;
            .filterMaskContentTwo {
              width: 240px;
              height: 320px;
              background: #ffffff;
              box-shadow: 0px 0px 4px 3px rgba(194, 194, 194, 0.25);
              border-radius: 4px;
              position: absolute;
              top: 360px;
              right: 180px;
              overflow-y: auto;
              box-sizing: border-box;
              padding: 12px 0 18px 0;
              overflow-x: hidden;
              .innerPrompt {
                color: #666;
                width: 100%;
                padding-left: 20px;
                margin-top: 12px;
              }
              .contentTwoItem {
                width: 100%;
                height: 36px;
                line-height: 36px;
                font-size: 14px;
                color: #333333;
                cursor: pointer;
                .mingzi {
                  width: 100%;
                  height: 36px;
                  box-sizing: border-box;
                  padding-left: 18px;
                }
              }
              .contentTwoItem:hover {
                background: rgba(95, 149, 247, 0.05);
              }
            }
          }
        }
      }
    }
  }
}
// 控制淡入淡出效果
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

总结

这里面需要注意的就是鼠标移入移出显示对应的删除小图标。思路大致就这样,敲代码不易,咱们共同努力。


水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负