最近项目有个需求,需要对穿梭框里面的数据进行框选。然而项目本身是基于ant-design-vue组件库的。antd的组件并不支持这个功能。
好在需求有相关实现的参考。那是一个jquery时代的老项目了。实现起来很nice,只需要使用最原始的select - option 表单标签就行了。因为浏览器本身支持select表单选项的框选多选等快捷操作。
于是事情变得简单了。
从最简单的例子开始写。
<select multiple>
<option value="1">选项1</option>
<option value="2">选项2</option>
</select>
给select设置multiple属性后,显示上就会变为列表。然而要用到穿梭框上,需要再美化一下。
接下来,我封装了一个组件。
<template>
<select multiple ref="selectRef" @change="onChange">
<option v-for="(item, key) in items" :key="key" :value="item.value" :style="optionStyle">
<slot name="render" v-bind="item">
{{ item.label }}
</slot>
</option>
</select>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
const emit = defineEmits(['change']);
const props = defineProps({
itemStyle: {
type: Object,
default() {
return {};
},
},
items: {
type: Array,
default() {
return [];
},
},
});
const optionStyle = computed(() => {
return props.itemStyle || {};
});
const onChange = (val) => {
const arr = [];
const length = val.target.selectedOptions.length;
for (let i = 0; i < length; i++) {
// value 为字符串, _value是原始值
arr.push(val.target.selectedOptions[i]._value);
}
emit('change', arr);
};
</script>
这是最简版的,选择列表从items参数传入,选择的变更通过change 事件提供出去。 随着开发的深入,还发现一些问题。当选择完数据移到另一侧列表的时候,虽然原来选择的数据移除了,但选择状态还呈现在列表中。这时就需要一个方法清除选择。
const selectRef = ref();
const resetSelected = () => {
let arr = [...selectRef.value.selectedOptions];
for (let i = 0; i < arr.length; i++) {
arr[i].selected = false;
}
};
defineExpose({
resetSelected,
});
列表组件写好了。构想一下最终要呈现的界面
先把template大致定下来
<template>
<div :class="`${prefixCls}__container`">
<div :class="`${prefixCls}__left ${prefixCls}__wrapper`">
<div :class="`${prefixCls}__title-con`">
<div :class="`${prefixCls}__title`">
{{ titles[0] || '所有项目' }}
</div>
<div :class="`${prefixCls}__number`">
({{ leftData.selectedKeys.length > 0 ? `${leftData.selectedKeys.length}/` : ''
}}{{ leftData.filterItems.length }})
</div>
</div>
<div :class="`${prefixCls}__search`" v-if="showSearch">
<a-input v-model:value="leftData.searchValue" allow-clear />
</div>
<OriginList
v-if="mode === 'origin'"
ref="leftoriginRef"
:items="leftData.filterItems"
@change="leftChange"
:item-style="itemStyle"
:style="listStyle"
>
<template #render="item" v-if="mode === 'origin'">
<slot name="render" v-bind="item"></slot>
</template>
</OriginList>
</div>
<div :class="`${prefixCls}__operations`">
<slot name="buttonBefore"></slot>
<div :class="`${prefixCls}__button`" @click="moveToRight">
<slot name="rightButton">
<a-button type="default">
<DoubleRightOutlined />
</a-button>
</slot>
</div>
<slot name="buttonCenter"></slot>
<div :class="`${prefixCls}__button`" @click="moveToLeft">
<slot name="leftButton">
<a-button type="default">
<DoubleLeftOutlined />
</a-button>
</slot>
</div>
<slot name="buttonAfter"></slot>
</div>
<div :class="`${prefixCls}__right ${prefixCls}__wrapper`">
<div :class="`${prefixCls}__title-con`">
<div :class="`${prefixCls}__title`">
{{ titles[1] || '已选项目' }}
</div>
<div :class="`${prefixCls}__number`">
({{ rightData.selectedKeys.length > 0 ? `${rightData.selectedKeys.length}/` : ''
}}{{ rightData.filterItems.length }})
</div>
</div>
<div :class="`${prefixCls}__search`" v-if="showSearch">
<a-input v-model:value="rightData.searchValue" allow-clear />
</div>
<OriginList
v-if="mode === 'origin'"
ref="rightoriginRef"
:items="rightData.filterItems"
@change="rightChange"
:item-style="itemStyle"
:style="listStyle"
>
<template #render="item" v-if="mode === 'origin'">
<span :style="itemStyle">
<slot name="render" v-bind="item"></slot>
</span>
</template>
</OriginList>
</div>
</div>
</template>
可以看到,左右两侧都分别有头部,搜索框,列表。
这两个列表有很多方法和状态是相同的。这时vue3 的composition Api 的优势就发挥出来了。
写一个方法,包含这些状态:
import { reactive, computed, watch } from 'vue';
export function useList() {
const data = reactive({
filterItems: [],
searchValue: '',
selectedKeys: [],
checkAll: false,
});
function selectedChange(val) {
data.selectedKeys = val;
}
return {
data,
selectedChange,
};
}
在穿梭框主体script上:
<script setup lang="ts" name="ExtTransfer">
import { ref, computed, watch, watchEffect } from 'vue';
import OriginList from './OriginList.vue';
import { useList } from './hooks/useList';
const props = defineProps({
showSearch: {
type: Boolean,
default: true,
},
dataSource: {
type: Array,
default() {
return [];
},
},
targetKeys: {
type: Array,
default() {
return [];
},
},
filterOption: {
type: Function,
default: filterOption,
},
listStyle: {
type: Object,
default() {
return {};
},
},
titles: {
type: Array,
default() {
return [];
},
},
itemStyle: {
type: Object,
default() {
return {};
},
},
});
const emit = defineEmits(['change']);
// 左侧框
const leftoriginRef = ref();
const { data: leftData, indeterminate: leftIndete, selectedChange: leftChange } = useList();
// 右侧框
const rightoriginRef = ref();
const { data: rightData, indeterminate: rightIndete, selectedChange: rightChange } = useList();
const targetKeys = ref([]);
const targetItems = computed(() => {
return props.dataSource.filter((item) => {
return targetKeys.value.includes(item.value);
});
});
watch(
() => props.targetKeys,
(val) => {
targetKeys.value = val;
},
{
immediate: true,
},
);
watchEffect(() => {
const leftSearch = leftData.searchValue;
const rightSearch = rightData.searchValue;
if (leftSearch.trim() === '') {
leftData.filterItems = props.dataSource.filter((item) => {
return !targetKeys.value.includes(item.value);
});
} else {
leftData.filterItems = props.dataSource.filter((option) => {
return !targetKeys.value.includes(option.value) && props.filterOption(leftSearch, option);
});
}
if (rightSearch.trim() === '') {
rightData.filterItems = [...targetItems.value];
} else {
rightData.filterItems = targetItems.value.filter((option) => {
return props.filterOption(rightSearch, option);
});
}
});
function moveToRight() {
leftoriginRef.value?.resetSelected();
targetKeys.value = [...targetKeys.value, ...leftData.selectedKeys];
leftData.selectedKeys = [];
emit('change', targetKeys.value);
}
function moveToLeft() {
const arr = [];
const length = targetKeys.value.length;
for (let i = 0; i < length; i++) {
const item = targetKeys.value[i];
if (!rightData.selectedKeys.includes(item)) {
arr.push(item);
}
}
targetKeys.value = arr;
rightData.selectedKeys = [];
rightoriginRef.value?.resetSelected();
emit('change', targetKeys.value);
}
function resetSearch() {
leftData.searchValue = '';
rightData.searchValue = '';
}
defineExpose({
resetSearch,
});
</script>
穿梭框在参数设计上,为了照顾使用习惯,尽量跟随ant design vue 穿梭框的参数,为了使代码简洁。使用watchEffet方法进行监听。这样,不管在搜索或者数据源变动时,列表都能刷新。