tabs.vue

<template>
  <div class="tabs">
    <div class="tabs__titles" ref="tabsTitlesRef">
    	<div class="tabs__titles-item"
         :class="{active : index === activeIndex}"
         @click="tabChange(item, index)"
         v-for="(item, index) in titles"
         :key="item.index">
         <div class="tabs__titles-item__line"></div>
         <slot v-if="$slots.title" name="title" :row="item"></slot>
         <div v-else class="tabs__titles-item__text">{{ item }}</div>
       </div>
     </div>
    <div class="tabs__content" :style="{'transform': translate}">
      <slot name="default"></slot>
    </div>
</div>
</template>

<script setup>
import {
  ref,
  useSlots,
  provide,
  defineProps,
} from 'vue';

// eslint-disable-next-line
const props = defineProps({
  modelValue: {
    default: '',
  },
});

const $slots = useSlots();
const titles = ref([]);
const tabsTitles = ref(null);
const activeIndex = ref(0);
const translate = ref('translateX(0%)');
const getTitles = (val) => {
  if (!$slots.title && typeof val !== 'string') {
    console.error('非自定义标题内容不允许传递对象类型');
  } else {
    titles.value.push(val);
  }
};
provide('getTitles', getTitles);
provide('activeKey', props.modelValue);

const scrollIntoView = (index) => {
  const { length } = document.querySelectorAll('.tabs__titles-item');
  const activeNodeNextNodeWidth = length === index + 1 ? 0 : document.querySelectorAll('.tabs__titles-item')[index + 1].offsetWidth;
  const width = window.innerWidth / 2 - activeNodeNextNodeWidth;

  let offset = 0;
  for (let i = length - 1; i > index + 1; i--) {
    offset += document.querySelectorAll('.tabs__titles-item')[i].offsetWidth;
  }

  if (offset > width) {
    document.querySelectorAll('.tabs__titles-item')[index].scrollIntoView({ inline: 'center' });
  } else {
    document.querySelectorAll('.tabs__titles-item')[length - 1].scrollIntoView({ inline: 'end' });
  }
};

const tabChange = (item, index) => {
  activeIndex.value = index;
  translate.value = `translateX(${-100 * index}%)`;
  scrollIntoView(index);
};
</script>

<style lang="scss">
.tabs {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.tabs__titles {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  background-color: #f8f8f8;
  flex-shrink: 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0 10px;
  scroll-behavior: smooth;
  .tabs__titles-item {
    width: auto!important;
    position: relative;
    flex: 1 0 auto;
    height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 70px;
    padding: 0 10px;
  }
}
.tabs__titles-item.active {
  font-weight: 600;
  .tabs__titles-item__line {
    width: 40px;
    transition: width .3s ease;
  }
}
.tabs__titles-item__line {
  position: absolute;
  bottom: 15%;
  left: 50%;
  width: 0;
  height: 3px;
  background: -webkit-gradient(linear,left top,right top,from(#fa2c19),to(rgba(250,44,25,.15)));
  background: linear-gradient(90deg,#fa2c19 0%,rgba(250,44,25,.15) 100%);
  transform: translate(-50%);
  overflow: hidden;
}
.tabs__content {
  display: flex;
  flex-wrap: nowrap;
  transition-duration: 300ms;
}
.tabs__titles::-webkit-scrollbar {
    display: none;
    width: 0;
    background: transparent;
}
</style>

tabpane.vue

<template>
  <div class="tabpane">
    <slot></slot>
  </div>
</template>

<script setup>
import {
  defineProps,
  inject,
  onMounted,
} from 'vue';

// eslint-disable-next-line
const props = defineProps({
  title: [String, Object],
  paneKey: [String, Number],
});

const getTitles = inject('getTitles');

onMounted(() => {
  getTitles(props.title);
});

</script>

<style>
.tabpane {
  width: 100%;
  padding: 20px;
  flex-shrink: 0;
  overflow: auto;
  height: 100%;
}
</style>

使用

<template>
  <tabs v-model="state.tabValue">
    <tab-pane title="tab1">tab1内容</tab-pane>
    <tab-pane title="tab2">tab2内容</tab-pane>
    <tab-pane title="tab3">tab3内容</tab-pane>
  </tabs>
</template>
<script setup>
import { reactive } from 'vue';
const state = reactive({
	tabValue: '0'
})
</script>
Logo

前往低代码交流专区

更多推荐