当前位置 : 主页 > 网络编程 > JavaScript >

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

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 概述 效果图 实现过程 组件分析 所需的前置知识 项目组件文件夹 Tabs.vue TabPane.vue render.js index.js 使用 总结 概述 前端项目中,多数页面涉及到选项卡切换,包括路由切换,指令v
目录
  • 概述
  • 效果图
  • 实现过程
    • 组件分析
    • 所需的前置知识
    • 项目组件文件夹
    • Tabs.vue
    • TabPane.vue
    • render.js
    • index.js
    • 使用
  • 总结

    概述

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

    效果图

    实现过程

    组件分析

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

    所需的前置知识

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

    项目组件文件夹

    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组件的资料请关注自由互联其它相关文章!

    上一篇:Bpmn.js 自定义描述文件使用说明
    下一篇:没有了
    网友评论