最近項目有個需求土铺,需要對穿梭框里面的數(shù)據(jù)進(jìn)行框選。然而項目本身是基于ant-design-vue組件庫的。antd的組件并不支持這個功能理郑。
好在需求有相關(guān)實現(xiàn)的參考。那是一個jquery時代的老項目了咨油。實現(xiàn)起來很nice您炉,只需要使用最原始的select - option 表單標(biāo)簽就行了。因為瀏覽器本身支持select表單選項的框選多選等快捷操作臼勉。
于是事情變得簡單了邻吭。
從最簡單的例子開始寫。
<select multiple>
<option value="1">選項1</option>
<option value="2">選項2</option>
</select>
給select設(shè)置multiple屬性后宴霸,顯示上就會變?yōu)榱斜泶亚纭H欢玫酱┧罂蛏希枰倜阑幌隆?/p>
接下來瓢谢,我封裝了一個組件畸写。
<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參數(shù)傳入枯芬,選擇的變更通過change 事件提供出去。 隨著開發(fā)的深入采郎,還發(fā)現(xiàn)一些問題千所。當(dāng)選擇完數(shù)據(jù)移到另一側(cè)列表的時候,雖然原來選擇的數(shù)據(jù)移除了蒜埋,但選擇狀態(tài)還呈現(xiàn)在列表中淫痰。這時就需要一個方法清除選擇。
const selectRef = ref();
const resetSelected = () => {
let arr = [...selectRef.value.selectedOptions];
for (let i = 0; i < arr.length; i++) {
arr[i].selected = false;
}
};
defineExpose({
resetSelected,
});
列表組件寫好了整份。構(gòu)想一下最終要呈現(xiàn)的界面
先把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>
可以看到待错,左右兩側(cè)都分別有頭部籽孙,搜索框,列表火俄。
這兩個列表有很多方法和狀態(tài)是相同的犯建。這時vue3 的composition Api 的優(yōu)勢就發(fā)揮出來了。
寫一個方法瓜客,包含這些狀態(tài):
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']);
// 左側(cè)框
const leftoriginRef = ref();
const { data: leftData, indeterminate: leftIndete, selectedChange: leftChange } = useList();
// 右側(cè)框
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>
穿梭框在參數(shù)設(shè)計上适瓦,為了照顧使用習(xí)慣,盡量跟隨ant design vue 穿梭框的參數(shù)忆家,為了使代碼簡潔犹菇。使用watchEffet方法進(jìn)行監(jiān)聽。這樣芽卿,不管在搜索或者數(shù)據(jù)源變動時揭芍,列表都能刷新。