Vue3嵌套组件封装实现

589 阅读1分钟

封装一个类似于El-tab的嵌套组件

  1. 容器组件通过 useSlots 获取到字组件的 props 来渲染tabs
  2. 通过 provide 注入当前 激活的 activeName
  3. 子组件通过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>