封装一个类似于El-tab的嵌套组件
- 容器组件通过 useSlots 获取到字组件的 props 来渲染tabs
- 通过 provide 注入当前 激活的 activeName
- 子组件通过inject来获取当前activeName 从而实现动态的渲染
CustomRadio.vue
<template>
<section class="custom-tab-container" :style="computedStyle">
<div
v-for="item in computedTabs"
@click="onChange(item)"
:key="item.value"
:class="['tab-item', currentActive === item.value && 'active']"
>
<div class="recommend-wrap">
<span class="item-label">{{ item.label }}</span>
<span class="recommendation-badge" v-if="item.isRecommend">推荐</span>
</div>
<el-radio :value="item.value" v-model="currentActive" />
</div>
</section>
<section>
<slot></slot>
</section>
</template>
<script setup lang="ts">
import { computed, ref, useSlots, provide, watch } from "vue"
import { TAB_ROOT_KEY } from "./constant"
const props = defineProps({
modelValue: {
type: [String, Number],
default: "",
},
})
// 核心是通过 useSlots 获取到所有的字组件的 props 来渲染tabs
const slots = useSlots()
const computedTabs = computed(() => {
return slots
.default()
.map(({ props }) => ({ label: props.label, value: props.name, isRecommend: props.isRecommend }))
})
const computedStyle = computed(() => {
return {
"grid-template-columns": `repeat(${computedTabs.value.length || 1}, 1fr)`,
}
})
const currentActive = ref(props.modelValue ?? 0)
const setCurrentActive = (value) => {
if (currentActive.value === value) return
currentActive.value = value
}
watch(
() => props.modelValue,
(modelValue) => setCurrentActive(modelValue)
)
const emits = defineEmits(["update:modelValue", "change"])
const onChange = async (item) => {
setCurrentActive(item.value)
emits("update:modelValue", item.value)
emits("change", item)
}
const provideModel = {
currentActive,
}
export type ProvideTabModel = typeof provideModel
provide(TAB_ROOT_KEY, {
currentActive,
})
</script>
<style lang="scss" scoped>
.custom-tab-container {
width: 100%;
display: grid;
grid-gap: 12px;
.tab-item {
border-radius: 4px;
border: 1px solid #e2e2e2;
color: #949494;
font: normal 400 14px "PingFangSC, PingFang SC";
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
&.active {
color: $primaryColor;
border: 1px solid $primaryColor;
background: #effff7;
}
.item-label {
margin-right: 12px;
}
.recommendation-badge {
display: inline-block;
background-color: #e45445;
color: white;
padding: 0px 6px;
font-size: 12px;
height: 20px;
line-height: 20px;
font-weight: bold;
border-radius: 0px 6px 6px 0px;
position: relative;
}
.recommendation-badge::before {
content: "";
position: absolute;
top: 0;
left: -10px;
width: 0;
height: 0;
border-style: solid;
border-width: 10px 10px 10px 0;
border-color: transparent #e45445 transparent transparent;
}
}
}
</style>
CustomRadioItem.vue
<template>
<div v-if="isShouldRender">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from "vue"
import { TAB_ROOT_KEY } from "./constant"
import { ProvideTabModel } from "./CustomRadio.vue"
const props = defineProps({
label: {
type: String,
default: "",
},
name: {
type: String,
default: "",
},
isRecommend: {
type: Boolean,
default: false,
},
})
const injectModel: ProvideTabModel = inject(TAB_ROOT_KEY)
const isShouldRender = computed(() => props.name === injectModel.currentActive.value)
</script>
<style></style>
使用
<template>
<div class="open-service-container">
<custom-radio v-model="activeName" @change="onChange">
<custom-radio-item label="tab1" name="1" :isRecommend="true">1</custom-radio-item>
<custom-radio-item label="tab2" name="2" :isRecommend="false">2</custom-radio-item>
<custom-radio-item label="tab3" name="3" :isRecommend="false">3</custom-radio-item>
</custom-radio>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import CustomRadio from "./CustomRadio.vue"
import CustomRadioItem from "./CustomRadioItem.vue"
const activeName = ref("3")
const onChange = (item) => {
console.log("🚀 ~ onChange ~ item:", item)
}
</script>
<style></style>