持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情
概述
因为最近项目中要使用穿梭框功能,目前使用的 UI 中不能满足需求,只好自己封装一个使用了,这里我把自己封装的和大家分享一下,希望能够得到大家的批评和建议。
我们先来看下完成的效果图
正文
首先我们新建一个项目,执行如下命令
pnpm create vite vue-transfer
选择 vue ,接着选择 vue-ts ,回车,等待项目安装完成, cd 进入项目根目录下,接着安装项目需要的依赖包
pnpm install
安装完成后,我们启动项目
pnpm run dev
控制台输出如下内容
vite v2.9.10 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
ready in 353ms.
说明项目正常启动了
这里我们还需要安装一下项目需要的一些第三方插件库,执行如下命令
pnpm install sass sass-loader
这里我们安装的 css 预处理器 sass ,当然你也可以选择 node-sass ,但是非常不建议使用, 因为 node-sass 和 nodejs 版本相关联,一旦本地 nodejs 版本升级, node-sass 就会无法工作,因为 nodes-sass 编译实际是由 libsass 完成的,而 libsass 是用 C/C++ 实现的,经常会出现安装失败的情况,又或者 nodejs 版本升级后 又需要重新安装 node-sass 情况。
如果你是在 docker 中使用 node-sass 还会经常出现因为缺少各种依赖导致 node-sass 编译失败情况,除了这些情况,使用 node-sass 还会因为经常出现需要的二进制文件下载失败的情况,导致编译失败,所以建议还是选择 sass
另外还需要说明的是 sass 不支持/deep/,要改成::v-deep 形式
穿梭框组件的 UI
安装完成后,我们删除项目里面多余的代码,在 src/components 下新建一个文件 transfer.vue, 这个文件就是我们的穿梭框组件,接着将穿梭框组件引入到 App.vue 中
<template>
<Transfer />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Transfer from './components/transfer.vue'
</script>
在 transfer.vue 文件中加入如下代码,开始把穿梭框的 UI 部分先做出来
<template>
<div class="transfer-wrap">
<div class="transfer-item transfer-left">
<div class="transfer-title">
<span class="transfer-title-checkbox">
<input v-model="leftAllChecked" type="checkbox" class="checkbox">源项</span>
<span>0/1</span>
</div>
<ul class="transfer-content">
<li class="transfer-content-item">
<input v-model="leftAllChecked" type="checkbox" class="checkbox" >Options
</li>
</ul>
</div>
<div class="transfer-middle">
<div><button type="button" class="btn tansfer-btn">右移</button></div>
<div><button type="button" class="btn tansfer-btn mt-10px">左移</button></div>
</div>
<div class="transfer-item transfer-right">
<div class="transfer-title">
<span class="transfer-title-checkbox">
<input v-model="leftAllChecked" type="checkbox" class="checkbox" > 目标项</span>
<span>0/0</span>
</div>
<ul class="transfer-content">
<li class="transfer-content-item"></li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
.transfer-wrap {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
font-size: 14px;
.transfer-title {
display: flex;
align-items: center;
padding: 8px 15px;
justify-content: space-between;
background-color: #fafafc;
}
.transfer-title-checkbox {
display: inline-flex;
align-items: center;
}
.transfer-item {
width: 230px;
height: 350px;
border: 1px solid #e9e9e9;
border-radius: 4px;
overflow: hidden;
}
.checkbox {
margin-right: 8px;
width: 18px;
color: #e9e9e9;
}
.transfer-content {
height: calc(350px - 39px);
overflow-y: auto;
}
.transfer-content-item {
padding: 8px 15px;
text-align: left;
display: inline-flex;
align-items: center;
width: 100%;
box-sizing: border-box;
cursor: pointer;
&:hover {
background-color: #f3f3f5;
}
}
.transfer-middle {
padding: 0 10px;
}
.btn {
margin: 0;
line-height: 1;
font-family: inherit;
padding: 10px 14px;
width: 50px;
display: inline-flex;
font-size: 14px;
border-radius: 4px;
color: #333639;
border: 1px solid #e0e0e6;
background-color: #0000;
white-space: nowrap;
outline: none;
justify-content: center;
user-select: none;
text-align: center;
cursor: pointer;
text-decoration: none;
&:hover {
border-color: #18a058;
}
}
.mt-10px {
margin-top: 10px;
}
}
</style>
UI 部分的效果图如下所示
左侧选择功能
接下来我们先来实现左侧的选择功能,左侧显示的数据是从 props 接收的数据,我们先定义接收的源数据的数据类型
import { ref, toRefs } from 'vue'
interface DataInter {
label: string | number;
value: string | number
}
const props = defineProps({
options: {
type: Array as PropType<DataInter[]>,
default: () => []
},
})
const { options } = toRefs(props)
这里我们还需要声明下 PropType, 在项目的根目录下建一个文件, types/global.d.ts,加入如下内容
import type { PropType as VuePropType } from "vue";
declare global {
declare type PropType<T> = VuePropType<T>;
}
这里是全局声明了 PropType
下一步打开 tsconfig.josn 文件,修改 inlcude 部分内容
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.d.ts",
保存后我们发现之前报错的 PropType 问题已经解决了。
打开 App.vue 文件,这里我们先定义一个假数据,并将数据传递到 Transfer 穿梭框组件。
<template>
<Transfer :options="data" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Transfer from './components/transfer.vue'
const data = ref([
{
label:"option1",
value:"1"
},
{
label:"option2",
value:"2"
},
{
label:"option3",
value:"3"
},
{
label:"option4",
value:"4"
},
])
</script>
在穿梭框组件中添加如下变量和函数
....
type selectedItemInter = string | number
const leftOptions = ref<DataInter[]>([])
leftOptions.value = options.value
// 左侧选中的数据
const leftSelectedData = ref<DataInter[]>([])
// checkbox 变量
const allChecked = ref(false)
// 左侧checkbox选中变量值
const leftIsCheckedData = ref<boolean[]>([])
// 左侧源数据checkbox 选中状态存储
function getLeftCheckedData(bool = false) {
leftIsCheckedData.value = []
for (let i = 0; i >= leftOptions.value.length; i++) {
leftIsCheckedData.value.push(bool)
}
}
// 初始化 leftSelectedData
getLeftCheckedData()
// 单个checkbox 选择功能
function toggleLeftCheckboxItem(e: MouseEvent, key: DataInter) {
if ((e.target as HTMLInputElement).checked) {
leftSelectedData.value.push(key)
} else {
let index = leftSelectedData.value.findIndex((item: DataInter) => item.value === key.value)
if (index < 0) {
leftSelectedData.value.splice(index, 1)
}
}
}
// 左侧全选
function toggleLeftAllCheckbox(e: MouseEvent) {
leftSelectedData.value = []
getLeftCheckedData((e.target as HTMLInputElement).checked)
if ((e.target as HTMLInputElement).checked) {
for (let key of leftOptions.value) {
leftSelectedData.value.push(key)
}
}
}
...
- toggleLeftAllCheckbox: 顶部
checkbox的点击事件,点击切换所有数据的选中/取消 - toggleLeftCheckboxItem: 单个
checkbox的点击事件,点击单个数据的选中/取消,当选中的数据数量等于所有源数据数量的时候,顶部的checkbox切换到选中状态,反之则取消 - getLeftCheckedData: 初始化
checkbox的选中/未选中状态事件
修改 左侧 UI 部分代码,把选择添加到相应的元素上
<div class="transfer-item transfer-left">
<div class="transfer-title">
<span class="transfer-title-checkbox">
<input v-model="allChecked" type="checkbox" class="checkbox" @click="toggleAllCheckbox">
源项
</span>
<span>{{ leftSelectedData.length }}/{{ leftOptions.length }}</span>
</div>
<ul class="transfer-content">
<li class="transfer-content-item" v-for="(item, index) in leftOptions" :key="item.value">
<input v-model="leftIsCheckedData[index]" type="checkbox" class="checkbox"
@click="toggleLeftCheckboxItem($event, item)">
{{ item.label }}
</li>
</ul>
</div>
如此,左侧的全选/单选功能我们就实现了
右侧选择功能
右侧的全选/单选功能和左侧的实现是一致的,这里我们就不多说了,代码如下
// 右侧选中的数据
const rightSelectedData = ref<DataInter[]>([])
//右侧数据
const rightAllChecked = ref(false)
const rightOptions = ref<DataInter[]>([])
const rightIsCheckedData = ref<boolean[]>([])
// 右侧选中checkbox初始化
function getRightCheckedData(bool = false) {
rightIsCheckedData.value = []
for (let i = 0; i < rightOptions.value.length; i++) {
rightIsCheckedData.value.push(bool)
}
}
// 右侧checkbox初始化
getRightCheckedData()
// 右侧单个checkbox选择功能
function toggleRightCheckboxItem(e: MouseEvent, key: DataInter) {
if ((e.target as HTMLInputElement).checked) {
rightSelectedData.value.push(key)
} else {
let index = rightSelectedData.value.findIndex((item: DataInter) => item.value === key.value)
if (index >= 0) {
rightSelectedData.value.splice(index, 1)
}
}
if (rightSelectedData.value.length === rightOptions.value.length) {
rightAllChecked.value = true
} else {
rightAllChecked.value = false
}
}
// 右侧全选
function toggleRightAllCheckbox(e: MouseEvent) {
rightSelectedData.value = []
getRightCheckedData((e.target as HTMLInputElement).checked)
if ((e.target as HTMLInputElement).checked) {
for (let key of rightOptions.value) {
rightSelectedData.value.push(key)
}
}
}
右侧 UI 部分代码
<div class="transfer-item transfer-right">
<div class="transfer-title">
<span class="transfer-title-checkbox">
<input type="checkbox" v-model="rightAllChecked" :disabled="rightOptions.length > 0 ? false : true " class="checkbox" @click="toggleRightAllCheckbox">
目标项
</span>
<span>{{rightSelectedData.length}}/{{rightOptions.length}}</span>
</div>
<ul class="transfer-content">
<li class="transfer-content-item" v-for="(item, index) in rightOptions" :key="item.value">
<input v-model="rightIsCheckedData[index]" type="checkbox" class="checkbox"
@click="toggleRightCheckboxItem($event, item)">{{ item.label }}</li>
</ul>
</div>
这里要说明的是我们要对右侧顶部 checkbox 的 disabled 状态做个判断, 没有数据的情况下使之不能选择
:disabled="rightOptions.length > 0 ? false : true "
数据穿梭功能
function toggleRight(){
let result:DataInter[] = [];
leftOptions.value.forEach((item: DataInter) => {
let index = leftSelectedData.value.findIndex((key: DataInter) => item.value === key.value)
console.log(index);
if (index < 0) {
result.push(item)
}
})
leftOptions.value = result
let arr = JSON.parse(JSON.stringify(leftSelectedData.value))
leftSelectedData.value = []
rightOptions.value = [...rightOptions.value, ...arr]
getLeftCheckedData()
if (leftOptions.value.length === 0) {
leftAllChecked.value = false
}
}
function toggleLeft(){
let result:DataInter[] = [];
rightOptions.value.forEach((item: DataInter) => {
let index = rightSelectedData.value.findIndex((key: DataInter) => item.value === key.value)
if (index < 0) {
result.push(item)
}
})
rightOptions.value = result
let arr = JSON.parse(JSON.stringify(rightSelectedData.value))
rightSelectedData.value = []
leftOptions.value = [...leftOptions.value, ...arr]
getRightCheckedData()
if (rightOptions.value.length === 0) {
rightAllChecked.value = false
}
}
上面两个函数主要实现了左侧选中数据穿梭到右边,右边选中数据穿梭到左边,需要注意的是需要判断下,如果将所有的数据都进行了穿梭,那就要将顶部 checkbox 的选中状态置为 false
绑定值更新
到这里,穿梭框大部分的功能我们已经实现了,现在就剩最后一步,更新绑定值
打开 App.vue 文件
修改组件为
<Transfer :options="data" v-model:value="value" @update:value="value = $event" />
这里主要接收绑定值的更新。
在穿梭框组件内部,添加
const emit = defineEmits(["update:value"]);
然后分别在 toggleRight 函数和 toggleLeft 函数的底部添加代码
const value = rightOptions.value.map((item:DataInter) => item.value)
emit("update:value", value)
ok 到这里我们的穿梭框功能就已经大功告成了
结语
虽然实现的功能很简单,但是自己在实现这个功能的过程中发现对象 vue3 ,和 Typescript 的使用理解更加的明白了。