概述

前端项目中,多数页面涉及到选项卡切换,包括路由切换,指令v-if等,本质上其实和选项卡切换思想差不多,如果是个简单的选项卡,还是很简单的,我们也不需要什么组件库的组件,自己也能几行代码写出来,但是涉及到动画,尺寸计算,拖拽的功能的时候,多数情况下,自己写还是要花点时间的,组件库就提供了现成的,拿来改改样式就行,为了对这个组件更加深入的理解,这里自己实现一个带拖拽,过渡的tabs组件。

效果图

vue选项卡Tabs组件实现示例详解

实现过程

组件分析

  • 组件包含两部分:Tabs组件和TabPane组件,参考绝大多数组件库的习惯
  • 组件主要分为需要点击的tab栏和下面对应的内容块
  • 我们需要对内容区和选项卡点击区分别加上过渡动画,提升用户体验
  • 最后需要加上拖拽调整选项卡顺序的功能

所需的前置知识

  • 熟悉vue内置transition组件
  • 深入掌握vue父子组件通信,除开emit和props,还需要掌握inject,emit和props,还需要掌握inject,emit和props,还需要掌握inject,parent,vnode,渲染函数等等,这些业务开发中用的不多,但是组件库里面比较常见。
  • 了解dom中位置计算和尺寸的基本计算
  • 熟悉html5新增拖拽相关事件

项目组件文件夹

vue选项卡Tabs组件实现示例详解

Tabs.vue

<template>
<div class="gnip-tab">
<div class="gnip-tab-nav">
<div
v-for="(item, index) in tabNavList"
@click.stop="handleTabNavClick(item, index)"
:class="['tab-nav-item', item.name == activeName ? 'active' : '']"
ref="tabNavItemRefs"
@drop="handleDrop(item, $event, index)"
@dragstart="handelDragstart(item, $event, index)"
@dragover="handleDragOver(item, $event, index)"
draggable="true"
>
<span v-if="item.text">{{ item.text }}</span>
<render v-if="item.renderFun" :renderFn="item.renderFun"></render>
</div>
</div>
<!-- 滚动滑块 -->
<div
class="tab-nav-track"
:style="{
background: showTrackBg ? '#e5e7eb' : '',
}"
>
<span
class="track-line"
:style="{ width: trackLineWidht + 'px', left: left + 'px' }"
></span>
</div>
<div class="tab-content-wrap">
<slot></slot>
</div>
</div>
</template>
<script>
// render组件,label为render函数的时候进行渲染
import Render from "./render";
export default {
props: {
// v-model的那项
value: {
type: String,
},
// 是否显示滑块背景
showTrackBg: {
type: Boolean,
default: false,
},
},
components: {
Render,
},
data() {
return {
// tab数组
tabNavList: [],
// 当前活跃项
activeName: "",
// 滑块的宽度
trackLineWidht: 0,
// 当前活跃索引
currentIndex: 0,
// 滑块偏移量
left: 0,
// 拖拽开始的哪项
dragOriginItemIndex: null,
// 拖拽活跃项的索引
dragStartIndex: null,
};
},
mounted() {
this.init();
},
methods: {
// 初始化
init() {
// 默认当前活跃项为外部v-model的值
this.activeName = this.value;
// 页面渲染任务之后计算滑块偏移量和宽度
this.$nextTick(() => {
this.currentIndex = this.$children.findIndex(
(component) => component.name == this.value
);
this.computedTrackWidth();
});
},
// 设置tab点击栏
setTabBar(tabsPaneInstance) {
// tab的描述信息可以是字符串也可以是render函数
const label = tabsPaneInstance.label,
type = typeof label;
// 添加到数组项中,根据添加条件渲染
this.tabNavList.push({
text: type == "function" ? "" : label,
renderFun: type == "function" ? label : "",
name: tabsPaneInstance.name,
});
},
handleTabNavClick(item, index) {
if (item.name == this.activeName) return;
// 更新当前活跃项
this.activeName = item.name;
// 活跃项的索引
this.currentIndex = index;
// 计算滑块的偏移量和宽度
this.computedTrackWidth();
},
// 计算滑块的偏移量和宽度
computedTrackWidth() {
// 插槽子组件的索引集合
const tabNavItemRefsList = this.$refs.tabNavItemRefs;
// 导航tab项的宽度
const scrollWidth = tabNavItemRefsList[this.currentIndex].scrollWidth;
// 滑块的宽度为scrollWidth
this.trackLineWidht = scrollWidth;
// 定位的偏移量为offsetLeft
this.left = tabNavItemRefsList[this.currentIndex].offsetLeft;
},
/*
关于拖拽请参考MDN文档: https://developer.mozilla.org/zh-CN/docs/Web/API/DragEvent,实现拖拽需要清楚关于拖拽相关的几个事件
*/
// 开始拖拽
handelDragstart(item, event, index) {
// 说明是拖拽的当前活跃的哪一项,记录这一项的索引位置
if (item.name == this.activeName) {
this.dragStartIndex = index;
}
this.dragOriginItemIndex = index;
},
// 推拽进入目标区域
handleDragOver(item, event) {
// 阻止默认事件
event.preventDefault();
},
//拖拽进入有效item
handleDrop(item, event, index) {
event.preventDefault();
// 说明拖动的位置是变了的
if (this.dragOriginItemIndex != index) {
// 交换数据,重新渲染生成tab栏
this.swap(this.dragOriginItemIndex, index);
// 重新计算滑块的偏移量
if (this.dragStartIndex !== null) {
this.currentIndex = index;
// 记住,数据更新为异步操作,因此我们这里需要用到nextTick,将计算任务放到渲染任务完成之后执行,避免计算不准确
this.$nextTick(() => {
this.computedTrackWidth();
this.dragStartIndex = null;
});
} else {
// 不是点击拖拽当前活跃项,也要重新计算滑块跨度和位置,因为每个tab项的宽度不一致,因此,每次拖拽都需要重新计算
this.$nextTick(() => {
this.computedTrackWidth();
});
}
// 这里还可以根据需要,发布一个拖拽完成事件
}
},
// 交换tab数据项
swap(start, end) {
let startItem = this.tabNavList[start];
let endItem = this.tabNavList[end];
// 由于直接通过索引修改数组,无法触发响应式,因此需要$set
this.$set(this.tabNavList, start, endItem);
this.$set(this.tabNavList, end, startItem);
},
},
};
</script>
<style lang="less">
.gnip-tab {
.gnip-tab-nav {
display: flex;
position: relative;
.tab-nav-item {
padding: 0 20px;
cursor: pointer;
line-height: 2;
}
}
.tab-nav-item.active {
color: #2d8cf0;
}
.tab-nav-track {
width: 100%;
position: relative;
height: 2px;
.track-line {
height: 2px;
background-color: #2d8cf0;
position: absolute;
transition: left 0.35s;
}
}
}
</style>

TabPane.vue

<template>
<div class="gnip-tabs-pane">
<transition :name="paneTransitionName">
<div class="tab-pane-content" v-show="$parent.activeName == name">
<slot name="default"></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
// tab项的文本或者render函数
label: {
type: [String, Function],
},
// 每项标识
name: {
type: String,
},
// 是否禁用当前项
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
paneTransitionName: "enter-right",
};
},
created() {
// 统一tab的数据给父组件进行处理和渲染
this.$parent.setTabBar(this);
},
};
</script>
<style lang="less">
.gnip-tabs-pane {
overflow-x: hidden;
.enter-right-enter-active {
transition: transform 0.35s;
}
.enter-right-enter {
transform: translateX(100%);
}
.enter-right-to {
transform: translateX(0);
}
}
</style>

render.js

主要用于将函数通过转化为render函数形式的组件(前提未提供模板)

export default {
name: "RenderCell",
props: {
renderFn: Function,
},
render(h) {
return this.renderFn(h);
},
};

index.js

按需导出组件

import TabPane from "./TabPane.vue";
export { Tabs, TabPane };

使用

App.vue

<template>
<div class="app">
<div class="aline">
<Tabs v-model="tabName" show-track-bg>
<TabPane label="首页" name="name1">首页</TabPane>
<TabPane label="图书详情页" name="name2" disabled>图书详情页</TabPane>
<TabPane label="个人主页" name="name3">个人主页</TabPane>
<TabPane :label="labelRender" name="name4">购物车</TabPane>
</Tabs>
</div>
</div>
</div>
</template>
<script>
import { Tabs, TabPane } from "@/components/Tabs";
export default {
components: { Tabs, TabPane },
data() {
return {
tabName: "name1",
labelRender(h) {
return h("div", "购物车");
},
};
},
};
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
}
.app {
padding: 20px;
button {
padding: 10px;
background-color: #008c8c;
color: #fff;
margin: 20px 0;
}
.container {
.operate {
text-align: center;
}
.aline {
width: 50%;
}
h2 {
font-weight: bold;
font-size: 20px;
}
.aline {
&:nth-child(1) {
margin-right: 20px;
}
}
display: flex;
justify-content: space-between;
}
}
.aline {
display: flex;
justify-content: center;
}
.item {
margin: 40px;
img {
width: 250px;
height: 200px;
}
ul {
margin: 0 auto;
li {
border: 1px solid red;
height: 200px;
width: 250px;
}
}
}
</style>

总结

通过上述组件的实现,对于HTML5拖拽事件的应用更加熟悉,关于拖拽请参考MDN文档: developer.mozilla.org/zh-CN/docs/…

以上就是vue选项卡Tabs组件实现示例详解的详细内容,更多关于vue选项卡Tabs组件的资料请关注本站其它相关文章!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。