在用 vue3.0 + TS + Antd 开发前端项目时,需要用到 Transfer 穿梭框,于是去 antd 官方网站查看,它的基本用法如下:
<template>
<div>
<a-transfer
v-model:target-keys="targetKeys"
v-model:selected-keys="selectedKeys"
:data-source="mockData"
:titles="['Source', 'Target']"
:render="item => item.title"
:disabled="disabled"
@change="handleChange"
@selectChange="handleSelectChange"
@scroll="handleScroll"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
interface MockData {
key: string;
title: string;
description: string;
disabled: boolean;
}
const mockData: MockData[] = [];
for (let i = 0; i < 20; i++) {
mockData.push({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 3 < 1,
});
}
const oriTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
const disabled = ref<boolean>(false);
const targetKeys = ref<string[]>(oriTargetKeys);
const selectedKeys = ref<string[]>(['1', '4']);
const handleChange = (nextTargetKeys: string[], direction: string, moveKeys: string[]) => {
console.log('targetKeys: ', nextTargetKeys);
console.log('direction: ', direction);
console.log('moveKeys: ', moveKeys);
};
const handleSelectChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
console.log('sourceSelectedKeys: ', sourceSelectedKeys);
console.log('targetSelectedKeys: ', targetSelectedKeys);
};
const handleScroll = (direction: string, e: Event) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
</script>
基本代码适配项目之后发现一个问题,穿梭框右侧添加的顺序是倒插的,从左边添加了一部分内容到右边,然后想要编辑修改再次添加,相关数据就会在最上面,项目要求右边列表有先后顺序,目前官方的代码没有给出自由排序的方法。于是我就去查看网友有没有相似的问题。发现可以自定义穿梭框,实现右侧数据自由拖拽功能。
实现原理为:
@dragstart 拖拽开始时在被拖拽元素上触发此事件
@drop.prevent 被拖拽的元素在目标元素上同时鼠标放开触发的事件,此事件作用在目标元素上
@dragover.prevent 拖拽元素在目标元素上移动的时候触发的事件,此事件作用在目标元素上
HTML 代码如下:
<template>
<div id="export-demo-edit">
<a-transfer v-model:target-keys="keys" :data-source="drawerData.mockData" :selected-keys="drawerData.selectedKeys"
show-search :filter-option="exportFilterOption" @change="handleChange"
@selectChange="onSelectChange" :list-style="{
width: '50%',
textAlign: 'left',
height: '350px',
}">
<template #children="{ direction, filteredItems, onItemSelect }">
<div v-if="direction === 'right'" class="transfer-left">
<div draggable="true" v-for="(item, index) in filteredItems" :key="item.key" @mouseenter="isTarget(item)"
@mouseleave="isTarget(item)" @dragstart="handleDragstart(index)" @drop.prevent="handleDrop()"
@dragover.prevent="handleDragover(index)" @click="() => checkChange(item.chosen, item.key, onItemSelect)"
class="transfer-left-item">
<a-checkbox v-model:checked="item.chosen"></a-checkbox>
<div class="content">
<span> {{ item.title }}</span>
<a-input-number v-if="item.key === 'progress'" :prefix="$t('setup.export.process')"
size="small" style="width: 60%; margin-left: 10px;" v-model:value="value" :min="1" :max="8"
onkeyup="value=value.replace(/^(0+)|[^\d]+/g,'')" @change="onchange" />
</div>
</div>
</div>
<div v-if="direction === 'left'" class="transfer-left">
<div v-for="item in filteredItems" :key="item.key" class="transfer-left-item" @click="() => checkChange(item.chosen, item.key, onItemSelect)">
<a-checkbox v-model:checked="item.chosen"></a-checkbox>
<span> {{ item.title }} </span>
<a-input-number v-if="item.key === 'progress'" :prefix="$t('setup.export.process')"
size="small" style="width: 60%; margin-left: 10px;" v-model:value="value" :min="1" :max="8"
onkeyup="value=value.replace(/^(0+)|[^\d]+/g,'')" @change="onchange" />
</div>
</div>
</template>
</a-transfer>
</div>
</template>
JavaScript 代码如下:
<script setup lang="ts">
import { onMounted, PropType, reactive, ref, toRefs } from 'vue'
import { exportFilterOption } from '@/logic/useExport'
import { MockData } from '@/interface/export-set'
import { useSetupStore } from '@/store/modules/setup'
const useSetup = useSetupStore()
const props = defineProps({
mockData: { type: Array as PropType<MockData[]>, default: null }
})
const { mockData } = toRefs(props)
const keys = ref()
const getKeys = () =>{
const arr: any[] = []
useSetup.exportTemplateInfo.fileds?.forEach((item:any)=>{
arr.push(item.key)
})
keys.value = arr
}
onMounted(() => {
getKeys()
})
const value = ref<number | undefined>(1)
value.value = useSetup.exportTemplateInfo.processNum ?? 1
const emits = defineEmits(['onchange', 'process'])
const onchange = (e: number) => {
if (e === null) {
value.value = 1
}
emits('process', value.value)
}
type drawData = {
oldItemIndex: String
newItemIndex: String
mockData: MockData[]
selectedKeys: String[]
}
const drawerData = reactive<drawData>({
oldItemIndex: '',
newItemIndex: '',
mockData: mockData.value,
selectedKeys: []
})
/**
* 选项选中时触发
*/
const handleChange = (key: MockData[], direction: string, moveKeys: string[]) => {
emits('onchange', keys.value)
}
// 所有标签数组
const allKeys: any = []
drawerData.mockData.forEach((items) => {
allKeys.push(items.key)
})
/**
* 选项改变时触发
*/
const onSelectChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
// 判断右边列表是否全选 是的话右边多选框全为true
if (targetSelectedKeys.length === keys.value.length && keys.value.length!==0) {
targetSelectedKeys.forEach((item:any) => {
drawerData.mockData.forEach((items: any, index: any) => {
if (item === items.key) {
items.chosen = true
}
});
});
}
// 判断右边列表是否全未选, 是的话左边多选框全为 false
if (targetSelectedKeys.length === 0) {
keys.value.forEach((item: any) => {
drawerData.mockData.forEach((items: any, index: any) => {
if (item === items.key) {
items.chosen = false
}
});
})
}
// 判断左边列表是否全选 是的话左边多选框全为 true
if (sourceSelectedKeys.length === drawerData.mockData.length - keys.value.length) {
sourceSelectedKeys.forEach((item: any) => {
drawerData.mockData.forEach((items: any) => {
if (item === items.key) {
items.chosen = true;
}
});
});
}
// 判断左边列表是否全未选, 是的话左边多选框全为 false
if (sourceSelectedKeys.length === 0) {
const newKeys = allKeys.filter((item: any) => keys.value.indexOf(item) === -1)
if (newKeys.length !== 0) {
newKeys.forEach((item: any) => {
drawerData.mockData.forEach((items: any) => {
if (items.key === item) {
items.chosen = false;
}
})
});
} else if (keys.value.length === 0) {
drawerData.mockData.forEach((items: any) => {
items.chosen = false;
})
}
}
drawerData.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys];
}
/**
* 拖动结束后触发
*/
const handleDrop = () => {
// 删除旧的
const changeItem = keys.value.splice(drawerData.oldItemIndex, 1)[0];
// 在列表中目标位置增加新的
keys.value.splice(drawerData.newItemIndex, 0, changeItem);
emits('onchange', keys.value)
};
/**
* 拖动开始后触发
*/
const handleDragstart = (index: any) => { drawerData.oldItemIndex = index; };
/**
* 拖动结束后触发
*/
const handleDragover = (index: any) => { drawerData.newItemIndex = index; };
// 用于判断选中了哪些多选框
const checkChange = (checked: any, key: any, onItemSelect: any) => {
drawerData.mockData.forEach((items: any) => {
if (key === items.key) {
items.chosen = true;
}
});
onItemSelect(key, !checked);
};
// 穿梭框列表图标显示与隐藏
const isTarget = (e: any) => {
let key = false;
keys.value.forEach((item: any) => { if (e.key === item) { key = item; } });
};
</script>