一. 最终实现
组件没有背景颜色, 为了凸显组件文字,才设置了背景颜色
二. 使用
<wq-tabs v-model="activeName" style="background:grey; padding: 20px">
<wq-tab-pane label="User" name="first">User</wq-tab-pane>
<wq-tab-pane label="Config" name="second">Config</wq-tab-pane>
<wq-tab-pane label="Role" name="third">Role</wq-tab-pane>
<wq-tab-pane label="Task" name="fourth">Task</wq-tab-pane>
</wq-tabs>
嘿嘿
三. 代码
type.ts
export interface WqTabItem {
label: string; // 标签
name: string; // 唯一标识
disabled?: boolean; // 是否禁用
}
constant.ts
export const WQ_TABS_PROVIDE = Symbol();
是的, 这个文件只有一行代码, 主要用于
tabs
组件 与tab-pane
之间使用依赖注入的标识
WqTabs.vue
<template>
<div class="wq-tabs">
<tab-nav :active-name="activeName" :on-tab-click="handleTabClick" :tab-item-arr="tabsArr"></tab-nav>
<slot></slot>
</div>
</template>
<script setup lang="tsx">
import { computed, onMounted, provide, ref, useSlots } from 'vue';
import tabNav from './wq-tab-nav.vue';
import { WQ_TABS_PROVIDE } from '@/components/WqTabs/constant';
import { WqTabItem } from '@/components/WqTabs/type';
type Props = {
modelValue: string;
beforeLeave?: (newTabName: string, oldTabName: string) => boolean | Promise<boolean>;
};
const props = withDefaults(defineProps<Props>(), { beforeLeave: () => true });
const emits = defineEmits<{ (name: 'tabClick', tabItem: any): void; (name: 'update:modelValue', value: string): void }>();
// 需要nav渲染的数据
const tabsArr = ref<any[]>([]);
// 当前激活的name
const activeName = computed({
get() {
return props.modelValue;
},
set(newVal) {
emits('update:modelValue', newVal);
},
});
// 将激活的名字提供给自组件使用
provide(WQ_TABS_PROVIDE, activeName);
const slots = useSlots();
const calcTabItems = () => {
if (slots.default) {
// 收集wq-tabs中间的插槽内容
let slotTabItemArr = slots.default();
// 收集他们的props数据
tabsArr.value = slotTabItemArr.map((item) => {
return { ...item.props };
});
} else {
tabsArr.value = [];
}
};
calcTabItems();
// 可能有异步
const setCurrentName = async (newTabName: string) => {
let oldTabName = activeName.value;
const res = await props.beforeLeave(newTabName, oldTabName);
if (res) {
activeName.value = newTabName; // 自身也更新一下
}
};
const handleTabClick = (tabItem: WqTabItem) => {
// 触发父组件的事件, 将当前点击的tab作为参数传给父组件
emits('tabClick', tabItem);
// 更新
setCurrentName(tabItem.name);
};
</script>
↑↑↑这里使用了useSlots()来获取插槽内容,
提取props中的数据, 交给nav进行渲染
WqTabNav.vue
<template>
<div class="wq-tab-nav">
<div
v-for="(tabItem, index) in tabItemArr"
:key="index"
:class="['wq-tab-nav-item', tabItem.name === activeName ? 'highLight' : '', tabItem.disabled ? 'isForbiddenItem' : '']"
@click="changeActiveName(tabItem)"
>
{{ tabItem.label }}
</div>
</div>
</template>
<script setup lang="ts">
import { WqTabItem } from '@/components/WqTabs/type';
type Prop = {
tabItemArr: WqTabItem[];
activeName: string;
onTabClick?: (tabItem: WqTabItem) => void;
};
const props = withDefaults(defineProps<Prop>(), {
tabItemArr: () => [],
activeName: '',
onTabClick: () => {},
});
// 更换激活
const changeActiveName = (tabItem: any) => {
// 点自己不执行
if (tabItem.name === props.activeName) {
return;
}
// 禁用不执行
if (tabItem.disabled) {
return;
}
props.onTabClick(tabItem);
};
</script>
<style lang="scss" scoped>
.wq-tab-nav {
width: 100%;
//border-bottom: 1px solid #e9e9e9;
&-item {
display: inline-block;
height: 40px;
line-height: 40px;
font-size: 14px;
font-weight: 500;
color: #303133;
margin: 0 12px;
cursor: pointer;
}
&-item:not(.isForbiddenItem):hover {
color: #409eff;
}
// 高亮项样式
.highLight {
color: #409eff;
border-bottom: 2px solid #409eff;
}
// 禁用项样式
.isForbiddenItem {
cursor: not-allowed;
color: #aaa;
}
}
</style>
wq-tab-pane.vue
<template>
<div v-show="isActive" class="wq-tab-pane">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed, inject, Ref } from 'vue';
import { WQ_TABS_PROVIDE } from '@/components/WqTabs/constant';
type Props = {
label: string;
name: string;
disable?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
disable: false,
});
// 激活状态
const isActive = computed(() => {
// 使用 inject 获取被激活的内容
const navActive = inject<Ref<string>>(WQ_TABS_PROVIDE);
const activeName = navActive?.value;
const currentName = props.name;
return activeName === currentName;
});
</script>
<style lang="scss" scoped>
.wq-tab-pane {
padding: 12px;
}
</style>
四. 缺陷
写的不是很灵活, 不能动态更新
若tab-pane是动态的, 会有nav不刷新的情况
嘿嘿~
等组件再实现的灵活一些再来填坑