前言
首先看看效果
在做需求的时候,本来是用element ui的# Transfer 穿梭框,后面发现数据量太大了,接口请求过来的字典数据直接5k+条(开始的时候后端就问了会不会存在数据过多的问题,项管说不会,我现在想揍她,mmp),dom渲染太多,直接卡死,寄,于是就叫后端改了字典接口为分页请求,而前端部分就通过这个组件来实现数据的选择,其中搜索和分页用于查询数据
功能
该组件能实现的效果是将数据展示出来并支持全选、反选、单选,以及选中的数据可以一键清除
效果
实现
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', [])
}
}
});