標籤頁是非常常用的元件,接下來我們來製作一個簡單的
Tabs
元件返回閱讀列表點選 這裡
需求分析
我們先做一個簡單的需求分析
- 可以選擇標籤頁排列的方向
- 選中的標籤頁應當有下劃線高亮顯示
- 切換選中時,下劃線應當有動畫效果
- 應當允許更換顏色
那麼可以整理出以下參數列格
引數 | 含義 | 型別 | 可選值 | 預設值 |
---|---|---|---|---|
direction | 方向 | string | row / column | row |
selected | 預設選中 | string | 子項的 name | 必填 |
color | 顏色 | string | 任意合法顏色值 | #d3c8f5 |
通過為子項設定 name
屬性,來指定預設值
骨架
本體
通過需求分析我們可以得到如下骨架:
<template>
<div
class="jeremy-tabs"
:style="{ '--color': color }"
ref="container"
:direction="direction"
>
<div class="jeremy-tabs-titles">
<button
v-for="(title, index) in titles"
:key="index"
class="jeremy-tabs-title"
:class="{ selected: names[index] === selected }"
@click="select(index)"
:ref="
(el) => {
if (names[index] === selected) {
selectedItem = el;
}
}
"
>
{{ title }}
</button>
<div class="jeremy-tabs-indicator" ref="indicator"></div>
</div>
<div class="jeremy-tabs-divider"></div>
<div class="jeremy-tabs-content">
<component :is="content" :key="selected" />
</div>
</div>
</template>
注意
這裡我們用一個
div
來充當下劃線,再使用一個新的component
來顯示使用者輸入的內容我們還需要為標籤頁建立子元件,即
Tab
元件
子元件
通過之前的分析,可以得出子元件 Tab
的骨架如下:
<template>
<div>
<slot></slot>
</div>
</template>
另外,我們還需要定義一個引數,也就是標籤的標題,所以還應該有如下宣告與匯出:
declare const props: {
title: string;
};
export default {
install: function (Vue) {
Vue.component(this.name, this);
},
name: "JeremyTab",
props: {
title: {
type: String,
default: "標籤頁",
},
},
};
功能
首先,我們先在 TypeScript
中宣告:
declare const props: {
direction?: "row" | "column";
selected: String;
color: String;
};
declare const context: SetupContext;
其次,再在 export default
中,寫入我們的引數:
export default {
name: "JeremyTabs",
props: {
direction: {
type: String,
default: "row",
},
selected: {
type: String,
required: true,
},
color: {
type: String,
default: "#8c6fef",
},
},
};
再次,再補全 setup
方法:
setup(props, context) {
if (!["row", "column"].includes(props.direction)) {
throw new Error("錯誤的方向");
}
const container = ref<HTMLDivElement>(null);
const selectedItem = ref<HTMLButtonElement>(null);
const indicator = ref<HTMLDivElement>(null);
const slots = context.slots.default();
slots.forEach((slot) => {
if (slot.type !== JeremyTab) {
throw new Error("一級子標籤必須是 JeremyTab");
}
if (!slot.props) {
throw new Error("存在 JeremyTab 屬性列為空");
}
if (!("title" in slot.props)) {
throw new Error("JeremyTab 缺少屬性 title");
}
if (!("name" in slot.props)) {
throw new Error("JeremyTab 缺少屬性 name");
}
});
const titles = slots.map((slot) => slot.props.title);
const names = slots.map((slot) => slot.props.name);
if (!names.includes(props.selected)) {
throw new Error("指定了不存在的 selected 值");
}
const content = computed(() =>
slots.find((slot) => slot.props.name === props.selected)
);
onMounted(() => {
watchEffect(
() => {
if (props.direction === "row") {
const { height } = selectedItem.value.getBoundingClientRect();
indicator.value.style.top = height + "px";
const { width } = selectedItem.value.getBoundingClientRect();
indicator.value.style.width = width + "px";
const left1 = container.value.getBoundingClientRect().left;
const left2 = selectedItem.value.getBoundingClientRect().left;
const left = left2 - left1;
indicator.value.style.left = left + "px";
} else {
const { height } = selectedItem.value.getBoundingClientRect();
indicator.value.style.height = height + "px";
const { width } = selectedItem.value.getBoundingClientRect();
indicator.value.style.left = width + "px";
const top1 = container.value.getBoundingClientRect().top;
const top2 = selectedItem.value.getBoundingClientRect().top;
const top = top2 - top1;
indicator.value.style.top = top + "px";
}
},
{ flush: "post" }
);
});
const select = (index) => {
context.emit("update:selected", names[index]);
};
return {
container,
selectedItem,
indicator,
slots,
titles,
names,
content,
select,
};
},
樣式表
最後,再補全樣式表
$theme-color: var(--color);
.jeremy-tabs {
display: flex;
flex-direction: column;
position: relative;
&-titles {
display: flex;
}
&-title {
padding: 4px 6px;
border: none;
cursor: pointer;
outline: none;
background: white;
&:focus {
outline: none;
}
&:hover {
color: $theme-color;
}
&.selected {
color: $theme-color;
}
}
&-indicator {
position: absolute;
transition: all 250ms;
border: 1px solid $theme-color;
}
&-divider {
border: 1px solid rgb(184, 184, 184);
}
&-content {
padding: 8px 4px;
}
}
.jeremy-tabs[direction="column"] {
flex-direction: row;
> .jeremy-tabs-titles {
flex-direction: column;
}
> .jeremy-tabs-content {
padding: 2px 10px;
}
}
測試
將 JeremyTabs
元件引入到測試文件,檢視一下執行效果
專案地址 ?
GitHub: https://github.com/JeremyWu917/jeremy-ui
官網地址 ?
JeremyUI: https://ui.jeremywu.top
感謝閱讀 ☕