在vue2中封装一个多选列表组件(类似element ui穿梭框)

55 阅读2分钟

前言

首先看看效果

image.png

在做需求的时候,本来是用element ui的# Transfer 穿梭框,后面发现数据量太大了,接口请求过来的字典数据直接5k+条(开始的时候后端就问了会不会存在数据过多的问题,项管说不会,我现在想揍她,mmp),dom渲染太多,直接卡死,寄,于是就叫后端改了字典接口为分页请求,而前端部分就通过这个组件来实现数据的选择,其中搜索和分页用于查询数据

功能

该组件能实现的效果是将数据展示出来并支持全选、反选、单选,以及选中的数据可以一键清除

效果

image.png

image.png

image.png

image.png

实现

vue3版本(一直在用vue2写,vue3忘得差不多了,很简陋的实现了下)

列表多选组件: multipleSelectList.vue

<!-- 列表多选组件 -->
<script setup lang="ts">
import { computed  } from 'vue'

interface Props {
  titles?: string[]; // 左右侧标题
  dataSource: Record<string, any>[]; // 源数据
  dataType: { label: string; value: string } // 展示和取值的数据结构
  modelValue: Record<string, any>[] // v-model绑定的value值
}
const props = withDefaults(defineProps<Props>(), {
  titles: () => ['文案一', '文案二'],
})
const emit = defineEmits<{
  (e: 'update:modelValue', value: [] | Record<string,any>[]): void
}>()

// 是否为全选状态
const isAllSelected = computed(() => props.dataSource.every(data => props.modelValue.map(item => item[props.dataType.value]).includes(data[props.dataType.value])))
// 全选按钮点击
const selectAllClick = () => {
  // dataSource为空,啥也不干
  if(!props.dataSource.length) {
    return
  }
  // value值为空, 直接全选,数组直接拉满,全部梭哈进去
  if(!props.modelValue.length) {
    return emit('update:modelValue', [...props.dataSource])
  }
  // 这里的逻辑是先判断是否是全选状态,如果是全选状态,则需要反选,否则就全选
  if(props.dataSource.every(data => props.modelValue.map(item => item[props.dataType.value]).includes(data[props.dataType.value]))) {
    // 1.已经是全选状态,需要反选(取消全选)
    const newArr = props.modelValue.filter(item => !props.dataSource.some(data => data[props.dataType.value] == item[props.dataType.value]))
    return emit('update:modelValue', newArr)
  }else {
    // 2.不是全选状态,需要全选
    const newArr = props.modelValue.filter(item => !props.dataSource.some(data => data[props.dataType.value] == item[props.dataType.value]))
    return emit('update:modelValue', [...newArr, ...props.dataSource])
  }
}
// 左侧内容区域单个内容点击
const contentClick = (item: Record<string, any>) => {
  const findIndex = props.modelValue.findIndex(content => content[props.dataType.value] == item[props.dataType.value])
      let newArr = []
      if(findIndex != -1) {
        newArr = [...props.modelValue]
        newArr.splice(findIndex, 1)
      }else {
        newArr = [...props.modelValue, item]
      }
      emit('update:modelValue', newArr)
}
// 是否选中
const isCheck = (item) => props.modelValue.some(content => content[props.dataType.value] == item[props.dataType.value])
// 清空所有选中
const closeAllClick = () => emit('update:modelValue', [])
// 删除选中
const handleClose = (item) => {
  const newArr = props.modelValue.filter(content => content[props.dataType.value] != item[props.dataType.value])
  emit('update:modelValue', newArr)
}
</script>

<template>
  <div class="multipleSelectList">
    <div class="multipleSelectListLeftContainer">
      <div class="containerHeader">
        <div class="content" @click.prevent.stop="selectAllClick">
          <el-checkbox 
            class="selectAll"
            :model-value="isAllSelected">
          </el-checkbox>
          <div class="title">{{ props.titles[0] || '' }}</div>
        </div>
      </div>
      <div class="contentList">
        <div 
          class="content" 
          v-for="item in props.dataSource" 
          :key="item[dataType.value]" 
          @click.prevent.stop="contentClick(item)"
          :title="item[dataType.label]">
          <el-checkbox :model-value="isCheck(item)">{{ item[dataType.label] }}</el-checkbox>
        </div>
      </div>
    </div>
    <div class="multipleSelectListRightContainer">
      <div class="containerHeader">
        <div class="content" @click.prevent.stop="closeAllClick">
          <el-icon class="closeAll"><CircleClose /></el-icon>
          <div class="title">{{ props.titles[1] || '' }}</div>
        </div>
      </div>
      <div class="contentList">
        <el-tag
          v-for="tag in props.modelValue"
          :key="tag[dataType.value]"
          closable
          @close="handleClose(tag)">
          {{ tag[dataType.label] }}
        </el-tag>
      </div>
    </div>
  </div>
</template>

<style lang="scss">
// 设置滚动条样式
@mixin scrollBarStyle($color, $size, $trackColor: #ECEEEF) {
  /*定义滚动条轨道 内阴影+圆角*/
  &::-webkit-scrollbar-track {
      -webkit-box-shadow: inset 0 0 6px rgba($color, 0.3);
      border-radius: 10px;
      background-color: $trackColor;
  }
  
  &::-webkit-scrollbar {
      // 宽高不一致会到导致elementUI table 列fixed时无法对齐
      width: $size;
      height: $size;
      background-color: transparent;
  }
  
  /*定义滑块 内阴影+圆角*/
  &::-webkit-scrollbar-thumb {
      -webkit-box-shadow: inset 0 0 8px rgba($color, .3);
      border-radius: 10px;
      background-color: $color;
  }
}
.multipleSelectList {
  width: 100%;
  height: 300px;
  display: flex;
  border: 1px solid #DCDFE6;
  border-radius: 5px;
  .multipleSelectListLeftContainer, .multipleSelectListRightContainer {
    width: 50%;
    .containerHeader {
      text-align: center;
      font-size: 16px;
      font-weight: 700;
      height: 32px;
      line-height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      .content {
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        cursor: pointer;
        .selectAll, .closeAll {
          margin: 0;
          margin-right: 5px;
        }
      }
    }
    .contentList {
      height: 260px;
      overflow: auto;
      @include scrollBarStyle(#ccc, 6px);
      .content {
        height: 45px;
        line-height: 45px;
        overflow: hidden;
      }
    }
  }
  .multipleSelectListLeftContainer {
    border-right: 1px solid #DCDFE6;
    .contentList {
      .content {
        display: flex;
        align-items: center;
        justify-content: center;
        padding-left: 10px;
        &:hover {
          background-color: #ecf5ff;
        }
        .el-checkbox {
          display: flex;
          align-items: center;
          flex: 1;
          .el-checkbox__input {
            display: block;
          }
          .el-checkbox__label {
            display: block;
            width: 270px;
            text-overflow: ellipsis;
            overflow: hidden;
            white-space: nowrap;
          }
        }
      }
    }
  }
  .multipleSelectListRightContainer {
    .contentList {
      text-align: center;
    }
  }
}
</style>

使用组件: app.vue

<script setup lang="ts">
import multipleSelectList from '@/components/multipleSelectList.vue'
import { ref } from 'vue';

const titles = ['备选单元', '巡检单元']
const dataType = {
  label: 'label',
  value: 'id',
}
const dataSource = ref([])
const form = ref({
  mySelectArr: []
})

const loading = ref(false)
const page = ref({
  pageSize: 20,
  pageNumber: 1,
  total: 10
})

const mockData = new Array(10000).fill(0).map((item, index) => ({
  id: index,
  item: item,
  label: `测试${index + 1}`,
  value: `测试${index + 1}`
}))

const mockData1 = mockData.slice(0,20)
const mockData2 = mockData.slice(20,40)
const mockData3 = mockData.slice(40,60)

const getDataSource = () => {
  return new Promise((resolve) => {
    loading.value = true
    setTimeout(() => {
      let result
      switch (page.value.pageNumber) {
        case 1:
          result = mockData1
          break;
        case 2:
          result = mockData2
          break;
        case 3:
          result = mockData3
          break;
      }
      dataSource.value = result
      loading.value = false
      resolve(result)
    }, 2000)
  })
}
const oneClick = () => {
  page.value.pageNumber = 1
  getDataSource()
}
const twoClick = () => {
  page.value.pageNumber = 2
  getDataSource()
}
const threeClick = () => {
  page.value.pageNumber = 3
  getDataSource()
}
const submit = () => {
  console.log('提交参数form为-----', form.value.mySelectArr)
}
// 获取数据源
getDataSource()
</script>

<template>
  <div class="container">
      <div class="form">
        <el-form>
          <el-form-item label="测试组件" label-width="120px" v-loading="loading">
            <multipleSelectList
              :dataSource="dataSource"
              :titles="titles"
              :dataType="dataType"
              v-model="form.mySelectArr">
            </multipleSelectList>
          </el-form-item>
        </el-form>
      </div>
      <div class="footer">
        <el-button type="primary" @click="oneClick">1</el-button>
        <el-button type="primary" @click="twoClick">2</el-button>
        <el-button type="primary" @click="threeClick">3</el-button>
        <el-button type="primary" @click="submit">提交</el-button>
      </div>
    </div>
</template>

<style scoped>
.container {
  width: 800px;
  height: 400px;
  margin: 0 auto;
}
</style>

vue2版本(熟悉的领域来了)

<!-- 列表多选组件 -->
<div class="multipleSelectList">
  <div class="multipleSelectListLeftContainer">
    <div class="containerHeader">
      <div class="content" @click.prevent.stop="selectAllClick">
        <el-checkbox 
          class="selectAll"
          :value="isAllSelected">
        </el-checkbox>
        <div class="title">{{ titles[0] || '' }}</div>
      </div>
    </div>
    <div class="contentList">
      <div 
        class="content" 
        v-for="(item,index) in dataSource" 
        :key="item[dataType.value]" 
        @click.prevent.stop="contentClick(item, index)"
        :title="item[dataType.label]">
        <el-checkbox :value="isCheck(item)">{{ item[dataType.label] }}</el-checkbox>
      </div>
    </div>
  </div>
  <div class="multipleSelectListRightContainer">
    <div class="containerHeader">
      <div class="content" @click.prevent.stop="closeAllClick">
        <i class="el-icon-circle-close closeAll"></i>
        <div class="title">{{ titles[1] || '' }}</div>
      </div>
    </div>
    <div class="contentList">
      <el-tag
        v-for="tag in value"
        :key="tag[dataType.value]"
        closable
        @close="handleClose(tag)">
        {{ tag[dataType.label] }}
      </el-tag>
    </div>
  </div>
</div>
.multipleSelectList {
  width: 700px;
  height: 300px;
  display: flex;
  border: 1px solid #DCDFE6;
  border-radius: 5px;
  .multipleSelectListLeftContainer, .multipleSelectListRightContainer {
    width: 350px;
    .containerHeader {
      text-align: center;
      font-size: 16px;
      font-weight: 700;
      height: 32px;
      line-height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      .content {
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        cursor: pointer;
        .selectAll, .closeAll {
          margin: 0;
          margin-right: 5px;
        }
      }
    }
    .contentList {
      height: 260px;
      overflow: auto;
      .content {
        height: 45px;
        line-height: 45px;
        overflow: hidden;
      }
    }
  }
  .multipleSelectListLeftContainer {
    border-right: 1px solid #DCDFE6;
    .containerHeader {
    }
    .contentList {
      .content {
        display: flex;
        align-items: center;
        justify-content: center;
        .el-checkbox {
          display: flex;
          align-items: center;
          flex: 1;
          .el-checkbox__input {
            display: block;
          }
          .el-checkbox__label {
            display: block;
            width: 270px;
            text-overflow: ellipsis;
            overflow: hidden;
            white-space: nowrap;
          }
        }
      }
    }
  }
  .multipleSelectListRightContainer {
    .contentList {
      text-align: center;
    }
  }
}
/*
 * @Author: zhengjianfeng
 * @Date: 2023-11-11 14:05:11
 * @LastEditTime: 2023-11-11 14:05:11
 * @Description: 列表多选组件
 */
const template = require("./content.html");
require("./index.scss");

module.exports = Vue.extend({
  name: "multipleSelectList",
  template: template,
  props: {
    dataSource: {
      type: Array,
      default: () => []
    },
    dataType: {
      type: Object,
      default: () => ({
        label: 'label',
        value: 'value'
      })
    },
    titles: {
      type: Array,
      default: () => ['文案一', '文案二']
    },
    value: {
      type: Array,
      default: []
    }
  },
  data() {
    return {

    }
  },
  computed: {
    isAllSelected() {
      return this.dataSource.every(data => this.value.map(item => item[this.dataType.value]).includes(data[this.dataType.value]))
    }
  },
  methods: {
    contentClick(item, index) {
      // console.log('contentClick------', item , index);
      const findIndex = this.value.findIndex(content => content[this.dataType.value] == item[this.dataType.value])
      let newArr = []
      if(findIndex != -1) {
        newArr = [...this.value]
        newArr.splice(findIndex, 1)
      }else {
        newArr = [...this.value, item]
      }
      this.$emit('update:value', newArr)
    },
    isCheck(item) {
      // console.log('isCheck------', item);
      // console.log('value------', this.value.some(content => content[this.dataType.value] == item[this.dataType.value]));
      return this.value.some(content => content[this.dataType.value] == item[this.dataType.value])
    },
    handleClose(item) {
      // console.log('handleClose------', item);
      const newArr = this.value.filter(content => content[this.dataType.value] != item[this.dataType.value])
      this.$emit('update:value', newArr)
    },
    // 全选按钮点击
    selectAllClick() {
      if(!this.dataSource.length) {
        return
      }
      if(!this.value.length) {
        return this.$emit('update:value', [...this.dataSource])
      }
      // 已经是全选状态(取消全选)
      if(this.dataSource.every(data => this.value.map(item => item[this.dataType.value]).includes(data[this.dataType.value]))) {
        const newArr = this.value.filter(item => !this.dataSource.some(data => data[this.dataType.value] == item[this.dataType.value]))
        return this.$emit('update:value', newArr)
      }else {
        // 全选
        const newArr = this.value.filter(item => !this.dataSource.some(data => data[this.dataType.value] == item[this.dataType.value]))
        return this.$emit('update:value', [...newArr, ...this.dataSource])
      }
    },
    closeAllClick() {
      return this.$emit('update:value', [])
    }
  }
});