| <template> | 
|   <div class="my-tabs"> | 
|     <div class="my-tabs__header p-0.5 mb-3 rounded bg-gray-100 cursor-pointers"> | 
|       <div class="my-tabs__header-shell relative flex justify-between"> | 
|         <div | 
|           v-for="(tab, index) in tabs" | 
|           :key="tab.props.label" | 
|           class="my-tab__title relative flex-auto py-1 text-center" | 
|           :class="{ 'my-active': tab.props.label === value }" | 
|           @click="onClickTab(tab, index)" | 
|         > | 
|           {{ tab.props.label }} | 
|         </div> | 
|   | 
|         <div class="my-tab__slider" :style="sliderStyle"></div> | 
|       </div> | 
|     </div> | 
|   | 
|     <div class="my-tabs__content"> | 
|       <slot /> | 
|     </div> | 
|   </div> | 
| </template> | 
|   | 
| <script> | 
| export default { | 
|   name: 'Tabs', | 
| }; | 
| </script> | 
|   | 
| <script setup> | 
| import { ref, reactive, onMounted, watch, nextTick } from 'vue'; | 
|   | 
| const props = defineProps({ | 
|   value: { | 
|     type: String, | 
|     required: true, | 
|   }, | 
| }); | 
| const emit = defineEmits(['update:value', 'change']); | 
|   | 
| watch( | 
|   () => props.value, | 
|   () => { | 
|     changeTab(); | 
|   } | 
| ); | 
|   | 
| const tabs = ref([]); | 
| const sliderStyle = reactive({ width: 0, left: 0 }); | 
| let tabWidth = 0; | 
| onMounted(() => { | 
|   // 初始化数据 | 
|   tabWidth = 100 / tabs.value.length; | 
|   sliderStyle.width = `${tabWidth}%`; | 
|   | 
|   changeTab(); | 
| }); | 
|   | 
| let preActiveTabVM = null; | 
| async function changeTab(index = -1) { | 
|   if (index < 0) { | 
|     index = tabs.value.findIndex((vm) => vm.props.label === props.value); | 
|   } | 
|   sliderStyle.left = `${tabWidth * index}%`; | 
|   | 
|   // 切换 tab 内容 | 
|   try { | 
|     await nextTick(); | 
|     preActiveTabVM?.exposed?.changeActive?.(false); | 
|     preActiveTabVM = tabs.value[index]; | 
|     preActiveTabVM.exposed?.changeActive?.(true); | 
|   } catch (error) { | 
|     console.log(error); | 
|   } | 
| } | 
|   | 
| function onClickTab(tab, index) { | 
|   emit('update:value', tab.props.label); | 
|   changeTab(index); | 
| } | 
|   | 
| defineExpose({ tabs }); | 
| </script> | 
|   | 
| <style scoped> | 
| .my-tabs__header { | 
|   margin-bottom: 0.75rem; | 
|   border-radius: 0.25rem; | 
|   --tw-bg-opacity: 1; | 
|   background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | 
|   padding: 0px; | 
|   padding: 0.125rem; | 
| } | 
|   | 
| .my-tabs__header-shell { | 
|   justify-content: space-between; | 
|   position: relative; | 
|   display: flex; | 
| } | 
|   | 
| .my-tab__title { | 
|   text-align: center; | 
|   position: relative; | 
|   flex: 1 1 auto; | 
|   padding-top: 0.25rem; | 
|   padding-bottom: 0.25rem; | 
|   z-index: 1; | 
| } | 
| .my-tab__title.my-active { | 
|   font-weight: bold; | 
| } | 
|   | 
| .my-tab__slider { | 
|   position: absolute; | 
|   bottom: 0px; | 
|   top: 0px; | 
|   border-radius: 0.25rem; | 
|   --tw-bg-opacity: 1; | 
|   background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | 
|   --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | 
|   --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); | 
|   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), | 
|     var(--tw-shadow); | 
|   transition-property: all; | 
|   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | 
|   transition-duration: 150ms; | 
|   transition-duration: 150ms; | 
| } | 
| </style> |