【组件封装】致敬 Ant Design:vue用原生代码打造一个 Tab 组件,不输专业级框架!(满足基本功能,去掉antd造成tab抖动、闪动的动画)

目录

第一章 前言

第二章 源代码(cv即可用)

2.1 搭建基础结构

2.1.1 html

2.1.2 css

2.2 实现具体功能

2.3 使用

第三章 总结


第一章 前言

在前端开发的世界里,Ant Design 是一个响当当的名字。它的 Tab 组件功能强大,界面美观,几乎成为了行业标准。但由于最近使用的过程中发现一个问题,在使用了多个tab的时候,屏幕分辨率变化,会造成tab抖动现象(网上也有许多解决方法,但是小编没解决)。于时,小编以antd为模板,挑战了一下,看看用原生代码能不能打造出一个既炫酷又实用的 Tab 组件!从而直接去掉造成抖动的动画效果。废话不多说,直接上源代码!!

  • 实现效果大致展示:

  • 每一个配置项目的效果看antd官网即可,基本上一致:

Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js

  • 区别(该省略号功能小编直接删除了,因为他是找出小编项目闪动的原因):

第二章 源代码(cv即可用)

2.1 搭建基础结构

  • 小编这里直接放的组件的源代码了,有问题可以留言/看代码解释
  • 其他技术支持文章:

【vue】v-bind=“$attrs“理解与使用-CSDN博客

【vue】slot插槽:灵活内容分发的艺术-CSDN博客

2.1.1 html

  • Tab 组件的核心是"标签页"和"内容区域"
<template>
  // 标签页
  // 1、tabPosition控制tab位置的配置项
  <div class="tabs" :class="`tabs-${tabPosition}`">
    // 注意这些动态样式,都是针对性的,如果tabPosition位置变化,边框的样式一样需要调整
    <div
      class="custom-tabs"
      :style="{
        fontSize: fontSize[size],
        borderTop: tabPosition === 'bottom' && tabBorder ? '1px solid #dadada' : '',
        borderBottom: tabPosition === 'top' && tabBorder ? '1px solid #dadada' : '',
        borderLeft: tabPosition === 'right' && tabBorder ? '1px solid #dadada' : '',
        borderRight: tabPosition === 'left' && tabBorder ? '1px solid #dadada' : ''
      }"
    >
      // centered 控制tab居中的样式
      <div :class="['tabs-header', { 'tabs-center': centered }]">
        // 以下提供了两种样式,line和card的
        <div
          v-for="pane in list"
          :key="pane.key"
          :class="[
            'tab-item',
            {
              active: activeKeyVal === pane.key,
              disabled: pane.disabled,
              'tab-animated': animated,
              'line-border-top':
                activeKeyVal === pane.key && tabPosition === 'bottom' && tabType === 'line',
              'line-border-bottom':
                activeKeyVal === pane.key && tabPosition === 'top' && tabType === 'line',
              'line-border-left':
                activeKeyVal === pane.key && tabPosition === 'right' && tabType === 'line',
              'line-border-right':
                activeKeyVal === pane.key && tabPosition === 'left' && tabType === 'line',
              'tab-card': tabType === 'card',
              'tab-card-active': activeKeyVal === pane.key && tabType === 'card',
              'card-border-top':
                activeKeyVal === pane.key && tabPosition === 'bottom' && tabType === 'card',
              'card-border-bottom':
                activeKeyVal === pane.key && tabPosition === 'top' && tabType === 'card',
              'card-border-left':
                activeKeyVal === pane.key && tabPosition === 'right' && tabType === 'card',
              'card-border-right':
                activeKeyVal === pane.key && tabPosition === 'left' && tabType === 'card'
            }
          ]"
          @click="handleClick(pane)"
          :style="{
            ...tabBarStyle,
            marginLeft: tabBarGutter + 'px'
          }"
        >
          // 默认只展示标题,如果有其他需求,支持插槽,自定义
          <slot name="tab" :pane="pane">{{ pane.title }}</slot>
          <!-- closable:控制单个标签是否可删除 -->
          <CloseOutlined
            class="closed"
            @click.stop="handleClose(pane)"
            v-show="
              pane.disabled !== true &&
              pane.closable !== false &&
              editable === true &&
              list.length > 1 &&
              activeKeyVal !== pane.key
            "
          />
        </div>
        <div class="tab-add" @click="handleAdd" v-if="editable">
          <PlusOutlined />
        </div>
      </div>
    </div>
    // 内容区域
    // 这里是展示内容的插槽,也也可以设置默认值什么的,小编这里是通过父组件有没有使用插槽控制是否展示内容
    <div class="tab-content">
      <div
        v-for="pane in list"
        :key="pane.key"
        :class="[
          { 'show-content': activeKey === pane.key, 'hide-content': activeKey !== pane.key }
        ]"
      >
        <slot name="content" :pane="pane"></slot>
      </div>
    </div>
  </div>
</template>

2.1.2 css

  • 主要是让标签页看起来像 Ant Design 那样整齐美观
  • 注意:这里面有样式是直接动态展示的,记得换掉:var(--main-6)
<style lang="less" scoped>
  .tabs {
    width: 100%;
    display: flex;
    flex-direction: column;
  }
  .custom-tabs {
    width: 100%;
    height: 44px;

    .tabs-header {
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;

      .tab-item {
        position: relative;

        .closed {
          position: absolute;
          top: 50%;
          right: 4px;
          margin-top: -7px;
          color: rgba(0, 0, 0, 0.45);
          display: none;
        }

        &:hover {
          .closed {
            display: block;
          }
        }

        &:first-child {
          margin-left: 0 !important;
        }
      }
    }

    .tabs-center {
      justify-content: center;

      .tab-item {
        flex: 1;
      }
    }

    .tab-item {
      height: 100%;
      cursor: pointer;
      padding: 0 5px;
      color: #000000e6;
      border-bottom: 3px solid transparent;
    }
  }

  // 卡片形式
  .tab-card {
    background-color: rgba(0, 0, 0, 0.02);
    border: 1px solid rgba(5, 5, 5, 0.06);
  }
  .tab-card.tab-item {
    border-bottom: 1px solid rgba(5, 5, 5, 0.06);
    gap: 5px;
  }
  .tab-card-active {
    background-color: #fff;
  }
  .tab-item.card-border-top {
    border-top-color: transparent;
  }
  .tab-item.card-border-bottom {
    border-bottom-color: transparent;
  }
  .tab-item.card-border-left {
    border-left-color: transparent;
  }
  .tab-item.card-border-right {
    border-right-color: transparent;
  }

  // 新增tab按钮
  .tab-add {
    width: 44px;
    height: 44px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    border: 1px solid rgba(5, 5, 5, 0.06);
    color: rgba(0, 0, 0, 0.88);
  }

  // 动画
  .tab-item.tab-animated {
    transition: all 0.2s linear;
  }

  // 激活时样式
  .tab-item.active {
    color: var(--main-6);
    font-weight: bold;
  }
  // 激活边框样式
  .tab-item.line-border-top {
    border-top: 3px solid var(--main-6);
  }
  .tab-item.line-border-bottom {
    border-bottom: 3px solid var(--main-6);
  }
  .tab-item.line-border-left {
    border-left: 3px solid var(--main-6);
  }
  .tab-item.line-border-right {
    border-right: 3px solid var(--main-6);
  }

  // tab禁用
  .tab-item.disabled {
    color: rgba(0, 0, 0, 0.25);
    cursor: not-allowed;
  }

  // 控制内容展示
  .show-content {
    display: block;
  }

  .hide-content {
    display: none;
  }

  // 位置
  .tabs-top {
    flex-direction: column;
  }

  .tabs-bottom {
    flex-direction: column-reverse;
  }

  .tabs-left {
    flex-direction: row;

    .custom-tabs {
      width: 150px;
      height: 100%;

      .tabs-header {
        width: 150px;
        height: 100%;
        display: flex;
        flex-direction: column;
        flex-wrap: wrap;

        .tab-item {
          width: 150px;
          padding: 8px 0;
        }
      }

      .tabs-center {
        .tab-item {
          display: flex;
          justify-content: center;
        }
      }
    }

    .tab-content {
      flex: 1;
    }
  }

  .tabs-right {
    flex-direction: row-reverse;

    .custom-tabs {
      width: 150px;
      height: 100%;

      .tabs-header {
        width: 150px;
        height: 100%;
        display: flex;
        flex-direction: column;
        flex-wrap: wrap;

        .tab-item {
          width: 150px;
          padding: 8px 0;
        }
      }

      .tabs-center {
        .tab-item {
          display: flex;
          justify-content: center;
        }
      }
    }

    .tab-content {
      flex: 1;
    }
  }
</style>

2.2 实现具体功能

小编主要实现了以下功能,提供以下配置项目(类型下面代码也携带):

  • list:tab列表
  • activeKey:激活索引
  • tabType:tab的两种形式
  • tabPosition:tab的位置
  • centered:是否居中,居中:每一个tab居中且均分
  • size:字体大小
  • tabBarStyle: tab bar样式,支持自定义
  • tabBarGutter:间隙(小编设置的范围)
  • tabBorder:tab边框:需要搭配 tabPosition展示 否则不生效
  • animated:是否使用动画
  • editable:是否可编辑, 注:激活标签不可删除,只剩一个标签也不可删除

为父组件调用提供了以下方法(具体参数看代码):

  • change:activeKey 的变化触发
  • tabClick:点击切换标签页触发
  • add:添加标签触发
  • delete:删除标签触发
<script setup name="XnCustomTabs">
  const props = defineProps({
    // tab列表
    list: {
      type: Array,
      required: true,
      default: () => [
        {
          key: 1,
          title: 'tab1'
        },
        {
          key: 2,
          title: 'tab2'
        },
        {
          key: 3,
          title: 'tab3'
        }
      ]
    },
    // 索引
    activeKey: {
      type: [String, Number],
      default: ''
    },
    // tab 的形式
    tabType: {
      type: String,
      default: 'line', // line, card
      validator: function (value) {
        return ['line', 'card'].includes(value)
      }
    },
    // tab与内容位置
    tabPosition: {
      type: String,
      default: 'top', // top, right, bottom, left
      validator: function (value) {
        return ['top', 'right', 'bottom', 'left'].includes(value)
      }
    },
    // tab 是否居中,居中:每一个tab居中且均分
    centered: {
      type: Boolean,
      default: false
    },
    // 字体大小
    size: {
      type: String,
      validator: function (value) {
        return ['small', 'middle', 'large'].includes(value)
      },
      default: 'middle'
    },
    // tab bar样式
    tabBarStyle: {
      type: Object,
      default: () => {}
    },
    // 间隙
    tabBarGutter: {
      type: Number,
      default: 0,
      validator: (value) => value >= 0 && value <= 100
    },
    // tab边框:需要搭配 tabPosition展示 否则不生效
    tabBorder: {
      type: Boolean,
      default: false
    },
    // 是否使用动画
    animated: {
      type: Boolean,
      default: true
    },
    // 是否可编辑, 注:激活标签不可删除,只剩一个标签也不可删除
    editable: {
      type: Boolean,
      default: false
    }
  })

  const emit = defineEmits(['change', 'tabClick', 'add', 'delete'])

  const activeKeyVal = ref(props.activeKey)
  const fontSize = {
    small: '12px',
    middle: '14px',
    large: '16px'
  }

  // 切换标签页
  const handleClick = (pane) => {
    if (pane.disabled) return
    emit('tabClick', pane.key)
  }

  // 添加标签
  const handleAdd = () => {
    emit('add', props.list)
  }

  // 删除标签
  const handleClose = (pane) => {
    const filterList = props.list.filter((item) => item.key !== pane.key)
    emit('delete', filterList, pane.key)
  }

  // 监听 activeKey 的变化
  watch(
    () => props.activeKey,
    (newVal) => {
      activeKeyVal.value = newVal
      emit('change', newVal)
    }
  )

  // 如果没有指定激活的标签页/初始化key不对,默认激活第一个
  onMounted(() => {
    const flag = props.list.some((item) => item.key === props.activeKey)
    if (!flag && props.list.length > 0) {
      activeKeyVal.value = props.list[0].key
    }
  })
</script>

2.3 使用

注意:由于小编要适配以前的以前的代码,这里是又做了一层二次封装,又再一次一个一个导入的,如果看了小编上面的技术支持文章其实是还有代码的优化空间的!!

<script setup name="XnNewTabs">
  import Tabs from '@/components/Tabs/index.vue'
  import tool from '@/utils/tool'

  const emit = defineEmits(['change', 'itemChange', 'tabClick', 'add', 'delete'])

  const props = defineProps({
    // 列表
    tabsList: {
      type: Array,
      default: () => [
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_basic_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_basic.png',
          title: '基础信息',
          key: 1
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_field_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_field.png',
          title: '字段配置',
          key: 2
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_form_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_form.png',
          title: '表单配置',
          key: 3
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_material_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_material.png',
          title: '材料配置',
          key: 4
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_situation_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_situation.png',
          title: '情形配置',
          key: 5
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_result_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_result.png',
          title: '结果页配置',
          key: 6
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_transact_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_transact.png',
          title: '办理流程绑定',
          key: 7
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_form_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_form.png',
          title: '业务办理信息配置',
          key: 8
        },
        {
          activedIcon: `@/assets/images/things/thing/${tool.getStepColorImg(
            'step_message_actived'
          )}.png`,
          icon: '@/assets/images/things/thing/step_message.png',
          title: '消息通知管理',
          key: 9
        }
      ]
    },
    activeKey: {
      type: [String, Number],
      default: 1
    },
    type: {
      type: String,
      default: 'line'
    },
    tabPosition: {
      type: String,
      default: 'top', // top, right, bottom, left
      validator: function (value) {
        return ['top', 'right', 'bottom', 'left'].includes(value)
      }
    },
    centered: {
      type: Boolean,
      default: true
    },
    size: {
      type: String,
      validator: function (value) {
        return ['small', 'middle', 'large'].includes(value)
      },
      default: 'middle'
    },
    tabBarStyle: {
      type: Object,
      default: () => {}
    },
    tabBarGutter: {
      type: Number,
      default: 0,
      validator: (value) => value >= 0 && value <= 100
    },
    tabBorder: {
      type: Boolean,
      default: false
    },
    animated: {
      type: Boolean,
      default: true
    },
    editable: {
      type: Boolean,
      default: false
    }
  })

  const change = (key) => {
    emit('change', key)
  }

  const itemChange = (item) => {
    emit('itemChange', item)
  }

  const tabClick = (key) => {
    emit('tabClick', key)
  }
  const tabAdd = (list) => {
    emit('add', list)
  }
  const tabDelete = (list, key) => {
    emit('delete', list, key)
  }
</script>
<template>
  <Tabs
    :list="tabsList"
    :activeKey="activeKey"
    @change="change"
    @tabClick="tabClick"
    @add="tabAdd"
    @delete="tabDelete"
    :tabType="type"
    :tabPosition="tabPosition"
    :size="size"
    :tabBarStyle="tabBarStyle"
    :tabBarGutter="tabBarGutter"
    :tabBorder="tabBorder"
    :animated="animated"
    :editable="editable"
    :centered="centered"
    class="tabs"
  >
    <!-- 自定义tab如果设置了样式配置将有可能不生效 -->
    <template #tab="{ pane }">
      <div
        class="tab_item h-full"
        @click="itemChange(pane)"
        :style="{
          color: pane.key === activeKey ? 'var(--main-6)' : ''
        }"
      >
        <span class="tab_icon">
          <img :src="tool.getAssetsFile(pane.activedIcon)" alt="" v-if="pane.key === activeKey" />
          <img :src="tool.getAssetsFile(pane.icon)" alt="" v-else />
        </span>
        {{ pane.title }}
      </div>
    </template>
    <!-- <template #content="{ pane }">{{ pane.key }}</template> -->
  </Tabs>
</template>

<style lang="less" scoped>
  .tabs {
    .tab_item {
      display: flex;
      justify-content: center;
      align-items: center;

      .tab_icon {
        width: 32px;
        height: 32px;
        margin-right: 5px;
      }
    }
  }
</style>

第三章 总结

        通过这次挑战,小编成功用原生代码实现了一个功能强大的 Tab 组件!尽管它可能没有 Ant Design 那么全面,但它的目前能实现的功能还是可以匹配的。更重要的是,我们在这个过程中掌握了一个新组件,将来不管在何处用到,都是可以节省不少的时间。

        如果大家觉得这个组件还不够完美,欢迎评论区留言,小编也会根据有用的需求继续改进!最后,前端开发的魅力就在于可以不断优化和创新!

        如果有用就一键三连吧!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值