10 - Vue3 UI Framework - Tabs 组件
标签页是非常常用的组件,接下来我们来制作一个简单的
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
感谢阅读
10 - Vue3 UI Framework - Tabs 组件的更多相关文章
- 05 - Vue3 UI Framework - Button 组件
官网基本做好了,接下来开始做核心组件 返回阅读列表点击 这里 目录准备 在项目 src 目录下创建 lib 文件夹,用来存放所有的核心组件吧.然后再在 lib 文件夹下创建 Button.vue 文件 ...
- 06 - Vue3 UI Framework - Dialog 组件
做完按钮之后,我们应该了解了遮罩层的概念,接下来我们来做 Dialog 组件! 返回阅读列表点击 这里 需求分析 默认是不可见的,在用户触发某个动作后变为可见 自带白板卡片,分为上中下三个区域,分别放 ...
- 08 - Vue3 UI Framework - Input 组件
接下来再做一个常用的组件 - input 组件 返回阅读列表点击 这里 需求分析 开始之前我们先做一个简单的需求分析 input 组件有两种类型,即 input 和 textarea 类型 当类型为 ...
- 09 - Vue3 UI Framework - Table 组件
接下来做个自定义的表格组件,即 table 组件 返回阅读列表点击 这里 需求分析 开始之前我们先做一个简单的需求分析 基于原生 table 标签的强语义 允许用户自定义表头.表体 可选是否具有边框 ...
- 11 - Vue3 UI Framework - Card 组件
卡片是非常常用也是非常重要的组件,特别是在移动端的众多应用场景中,随便打开一个手机 App ,您会发现充斥着各种各样的卡片. 所以,我们也来制作一个简易的 Card 组件 返回阅读列表点击 这里 需求 ...
- 00 - Vue3 UI Framework - 阅读辅助列表
阅读列表 01 - Vue3 UI Framework - 开始 02 - Vue3 UI Framework - 顶部边栏 03 - Vue3 UI Framework - 首页 04 - Vue3 ...
- 01 - Vue3 UI Framework - 开始
写在前面 一年多没写过博客了,工作.生活逐渐磨平了棱角. 写代码容易,写博客难,坚持写高水平的技术博客更难. 技术控决定慢慢拾起这份坚持,用作技术学习的阶段性总结. 返回阅读列表点击 这里 开始 大前 ...
- 04 - Vue3 UI Framework - 文档页
官网的首页做完了,接下来开始做官网的文档页 返回阅读列表点击 这里 路由设计 先想想我们需要文档页通向哪些地方,这里直接给出我的设计: 所属 子标题 跳转路径 文件名(*.vue) 指南 介绍 /do ...
- 12 - Vue3 UI Framework - 打包发布
基础组件库先做到这个阶段,后面我会继续新增.完善 接下来,我们对之前做的组件进行打包发布到 npm 返回阅读列表点击 这里 组件库优化 通用层叠样式表 我想大家都注意到了,前面我们在写组件的时候,sc ...
随机推荐
- [bzoj1032]祖码
先将连续的一段相同的点连起来,然后考虑对于一段区间,分两种情况:1.首尾两点再同时消掉,必然要先将去掉首尾的一段消掉后再消2.首尾两点再不同时刻消掉,那么必然存在一个断点k,使得k左右无关(题目中的错 ...
- 【备考06组01号】第四届蓝桥杯JAVA组A组国赛题解
1.填算式 (1)题目描述 请看下面的算式: (ABCD - EFGH) * XY = 900 每个字母代表一个0~9的数字,不同字母代表不同数字,首位不能为0. 比如 ...
- 快速入门上手Markdown
第一次接触Markdown是写代码初期看很多大佬的github,他们的项目一定会有一份文件叫Readme.md的文件 他们由一些简单美观的符号和汉字字母组成,编译之后成为一篇简单直观的文档 深入了解之 ...
- [GZOI2017]配对统计
发现我们可以在\(O(n)\)里很多处理出至多\(2n\)对好对. 然后转化成二维偏序. 然后想怎么做怎么做:排序+BIT,莫队都行.
- Codeforces 516D - Drazil and Morning Exercise(树的直径+并查集)
Codeforces 题目传送门 & 洛谷题目传送门 这是一道 jxd 的作业题,感觉难度不是特别大(虽然我并没有自己独立 AC,不过也可能是省选结束了我的脑子也没了罢(((,就随便写写罢 u ...
- 第42篇-JNI引用的管理(1)
在本地函数中会使用Java服务,这些服务都可以通过调用JNIEnv中封装的函数获取.我们在本地函数中可以访问所传入的引用类型参数,也可以通过JNI函数创建新的 Java 对象.这些 Java 对象显然 ...
- 【讲座】朱正江——基于LC-MS的非靶向代谢组学
本次课程主题为<基于LC-MS的非靶向代谢组学>,主要分为代谢组学简介.代谢组学技术简介.非靶向代谢组学方法和数据采集.非靶向代谢组学数据分析和代谢物结构鉴定几个方面. 一.代谢组简介 基 ...
- 07 MySQL安装图解--Windows版本
MySQL安装图解 使用微信扫码关注微信公众号,并回复:"MySQL环境",免费获取下载链接! 1.安装MySQL 2.校验MySQL 3.登录MySQL 登录MySQL:mysq ...
- MIT6.824 分布式系统实验
LAB1 mapreduce mapreduce中包含了两个角色,coordinator和worker,其中,前者掌管任务的分发和回收,后者执行任务.mapreduce分为两个阶段,map阶段和red ...
- flink-----实时项目---day05-------1. ProcessFunction 2. apply对窗口进行全量聚合 3使用aggregate方法实现增量聚合 4.使用ProcessFunction结合定时器实现排序
1. ProcessFunction ProcessFunction是一个低级的流处理操作,可以访问所有(非循环)流应用程序的基本构建块: event(流元素) state(容错,一致性,只能在Key ...