【VUE3.0】动手做一套像素风的前端UI组件库---Radio

目录

  • 引言
  • 做之前先仔细看看UI设计稿
    • 解读一下都有哪些元素:
    • 参考下成熟的组件库,看看还需要做什么?
  • 代码编写
    • 1. 设计group包裹选项的组件
      • group.vue
      • item.vue
    • 2. 让group的v-model和item的value联动起来
    • 3. 完善一下item的指示器样式
    • 4. 补充禁用模式和change事件
    • 5. 看下最终效果
    • 6. 组件完整代码
      • group.vue
      • item.vue
    • 7. 组件调用方式
  • 总结

引言

本教程基于前端UI样式库 NES.css 的UI设计,自行研究复现。欢迎大家交流优化实现方法~

此次组件库开发基于vue3框架,框架基础搭建过程以及基础素材准备参考:【VUE3.0】动手做一套像素风的前端UI组件库—先导篇

本篇复现的组件为radio,日常项目中较为常见的组件,主要涉及到的内容有:

  1. 参考NES.css进行基础样式构建,使用css模拟箭头指示器。
  2. 点击动效设计,利用animationanimation-timing-function属性模拟指示器闪动效果。
  3. 参考组件库Element Plus的设计使用方式,利用vue的slot插槽复现组件调用方式。
  4. 设置禁用模式和change事件。

做之前先仔细看看UI设计稿

在这里插入图片描述

解读一下都有哪些元素:

  • 抛开暗色版本的不谈,这个组件的内容其实蛮少的。
  • 我需要一组单选框,可以两个或者更多,互相之间互斥。
  • 在选中的选项旁边有一个闪动的指示器,有股红白机游戏开头设置的感觉。
  • 其他没什么特殊的。

参考下成熟的组件库,看看还需要做什么?

这里我们参考element plus
在这里插入图片描述

  • 使用group将选项包裹起来,通过v-model去双向绑定选择的项目value。
  • 作为一个组件,应该有一个禁用标识。
  • 作为一个组件,应该有个change事件,监控选择时候的响应值。
  • 其他的颜色控制、大小控制等属性我们舍弃,因为我们的组件库是个定制样式的组件库,不跟市面上的通用组件库攀比。

代码编写

按照设计稿解读内容以及其他组件库的参考:

1. 设计group包裹选项的组件

参考element的调用组件方式,思考下组件该怎么写能够达到这种调用方式?没错这里我们利用vue的slot去设计。
group

在components文件夹下创建radio文件夹。分别创建group.vue和item.vue并引入install.js注册全局组件。这里不清楚的回看:先导篇。
依照上图的设计逻辑,编写group和item的vue模板文件。

group.vue

首先写好html部分,设置基础样式类和插槽:

<template><div class="radio_group"><slot></slot></div>
</template>
<style scoped>
.radio_group {padding: 10px;display: flex;gap: 15px;user-select: none;
}
</style>

设置group组件的v-model双向绑定,在vue3的自定义组件中可以设置一个或多个双向绑定属性,组件标签使用v-model:attribute绑定变量,组件内部使用defineProps接受attribute,并且使用defineEmits(["update:attribute"])向父组件更新变量。(以上提到的attribute均为自定义变量名,不要混淆了),假如自定义组件仅有一个需要双向绑定的属性,那么可以直接在标签使用v-model绑定变量,组件内部会默认传入一个属性名为modelValue,更新变量和之前一样。(注意这里的modelValue是一个vue中确定的名字,不可以改

这里需要v-model的属性为任意基本类型, type设置为 [String, Number, Boolean],这些应该够了,反正element是这三种。

const props = defineProps({modelValue: {type: [String, Number, Boolean],default: "",},
});const emits = defineEmits(["update:modelValue"]);

item.vue

利用弹性盒子将指示器和label文字设置好布局关系。我不希望label可以无限长,所以设置了最大文字长度截断,并采用title属性让鼠标悬浮时显示全部文字,由于这个tooltip过于简陋,后期会专门制作tooltip组件去替换这里的title属性。
html部分,设置基础样式类和插槽:

<template><div class="radio"><span class="radio_arrow"></span><span class="radio_label" :title="slotText"><slot>option</slot></span></div>
</template>
<style scoped>
.radio {display: flex;justify-content: center;align-items: center;gap: 8px;
}
.radio_arrow {width: 16px;height: 16px;position: relative;
}.radio_label {max-width: 150px;white-space: nowrap; /* 确保文本在一行内显示 */overflow: hidden; /* 隐藏溢出的内容 */text-overflow: ellipsis; /* 使用省略号表示文本溢出 */
}
</style>

由于单选项的label是插槽提供的文本,默认是option,那么我该如何获取到slot填写的内容呢?
这里是利用到vue的useSlots的hooks函数,拿到slot内容。
以下是js部分:

<script setup>
import { ref, useSlots } from "vue";
const props = defineProps({value: {type: [String, Number, Boolean],default: "",},
});
// 设置过长文本提示词,利用useSlots抓取slot内容。
const slots = useSlots();
const slot = slots.default ? slots.default() : undefined;
const slotText = ref("");
if (slot) {slotText.value = slot[0].children;
} else {slotText.value = "option";
}
</script>

2. 让group的v-model和item的value联动起来

现在面临一个问题,group和item的数据如何联动起来,我需要用一个什么样的联动方式?

  • 首先声明一个点,虽然group和item是设计逻辑上的父子关系,但是并是真正的父子组件,他们只是通过插槽安放在一起,还是属于两个独立的组件。显然这里不能使用props和emit去父子组件传值,更新group的v-model的状态。
  • 使用全局状态,例如vuex、pinia等。这当然可以解决问题,但是未免有点太奢侈了,而且组件调用的地方有很多,需要创建多个全局状态去管理。不合适
  • 使用hooks函数,这也是个不错的选择。但是hooks的状态是一次性的,在group和item中注册两遍,两边的数据是互相封闭的,无法真正联动起来。如果是将状态放在闭包函数之外,倒也是可以做到数据共享,那其实和使用vuex这些全局状态没什么区别,反倒是需要增加动态id去区分,工作量巨大。不合适
  • 本次使用vue的依赖注入去解决问题,provide和inject

在group.vue中提供radio状态值和更新状态值的方法

import { ref, provide } from "vue";
const radioValue = ref(props.modelValue);
const updateRadioValue = (value) => {radioValue.value = value;emits("update:modelValue", value);
};
provide("radioValue", {radioValue,updateRadioValue,
});

在item.vue中截获依赖注入,并更新选项指示器的样式
在item中通过点击选项,触发更新radioValue的updateRadioValue方法,更新全局的radioValue状态,又因为radioValue是一个响应式变量,通过判断当前item的value值是否等于radioValue,来确认当前的选项是否被选中,从而更新当前选项的样式。

<template><div @click="checkRadio" class="radio"><spanclass="radio_arrow":class="{ radio_arrow_check: radioValue === value}"></span><span class="radio_label" :title="slotText"><slot>option</slot></span></div>
</template>
<script setup>
import { ref, inject} from "vue";
const { radioValue, updateRadioValue} = inject("radioValue");
const checkRadio = () => {if (radio_disabled.value) return;updateRadioValue(props.value);
};
</script>

至此组件的传值问题已经解决了。

3. 完善一下item的指示器样式

原本我是制作了一张指示器的icon图片,后来忍不住去 借鉴了一下NES.css是怎么做的指示器icon,发现居然可以使用box-shadow去完成效果。
这里还需要注意几个点:
- keyframes只设置到50%就可以了。
- animation需要设置steps(1)
解释下原因,如果你按照常规的方式设置keyframes到100%,不加steps(1),你会得到一个非常丝滑的动画效果,丝滑的有点过头,但是红白机的闪动方式是跳跃式的,不连贯的。按照如下设置后,才能得到想要的闪动效果。后续我会出一篇关于css的animation的使用详解,会涉及到这个概念。

@keyframes blink {0% {opacity: 1;}50% {opacity: 0;}
}
.radio_arrow {width: 16px;height: 16px;position: relative;
}
.radio_arrow_check::before {position: absolute;top: 0;left: 0;transform: translateY(-50%);content: "";width: 2px;height: 2px;color: #212529;box-shadow: 2px 2px, 4px 2px, 2px 4px, 4px 4px, 6px 4px, 8px 4px, 2px 6px,4px 6px, 6px 6px, 8px 6px, 10px 6px, 2px 8px, 4px 8px, 6px 8px, 8px 8px,10px 8px, 12px 8px, 2px 10px, 4px 10px, 6px 10px, 8px 10px, 10px 10px,2px 12px, 4px 12px, 6px 12px, 8px 12px, 2px 14px, 4px 14px;animation: blink 1s infinite steps(1);
}

先看下这一步的效果
在这里插入图片描述

4. 补充禁用模式和change事件

  • 在group组件中需要接收一个disabled的变量,控制组件变灰一些并且切换手势为禁用。
  • 向item传递这个disabled变量,禁用点击事件,让闪动停止,并且设置item的禁用手势。
  • 在group组件中updateRadioValue方法里向外emit一个change事件,将此刻的radioValue值传递出去。

在group.vue文件中做如下修改:

<template><div class="radio_group" :class="{ disabled: disabled }"><slot></slot></div>
</template><script setup>
import { ref, provide } from "vue";
const props = defineProps({disabled: {type: Boolean,default: false,},
});const emits = defineEmits(["change"]);
const radio_disabled = ref(props.disabled);
const updateRadioValue = (value) => {emits("change", value);
};
provide("radioValue", {radio_disabled,
});
</script>
<style scoped>
.disabled {opacity: 0.5;cursor: var(--cursor_disabled);
}
</style>

在item.vue文件中做如下修改:

<template><div @click="checkRadio" class="radio"><spanclass="radio_arrow":class="{ radio_arrow_check_disabled: radio_disabled }"></span><span class="radio_label" :title="slotText"><slot>option</slot></span></div>
</template><script setup>
import { ref, inject} from "vue";
const { radio_disabled } = inject("radioValue");
const cursorStyle = radio_disabled.value? "var(--cursor_disabled)": "var(--cursor_pointer)";
const checkRadio = () => {
// 当禁用标识传递进来后,禁用此方法if (radio_disabled.value) return;updateRadioValue(props.value);
};
</script>
<style scoped>
.radio {cursor: v-bind(cursorStyle);
}
.radio_arrow_check_disabled::before {animation: none;
}
</style>

5. 看下最终效果

在这里插入图片描述

6. 组件完整代码

group.vue

<template><div class="radio_group" :class="{ disabled: disabled }"><slot></slot></div>
</template><script setup>
import { ref, provide } from "vue";
const props = defineProps({modelValue: {type: [String, Number, Boolean],default: "",},disabled: {type: Boolean,default: false,},
});const emits = defineEmits(["update:modelValue", "change"]);
const radio_disabled = ref(props.disabled);
const radioValue = ref(props.modelValue);
const updateRadioValue = (value) => {radioValue.value = value;emits("update:modelValue", value);emits("change", value);
};
provide("radioValue", {radioValue,updateRadioValue,radio_disabled,
});
</script>
<style scoped>
.radio_group {padding: 10px;display: flex;gap: 15px;user-select: none;
}
.disabled {opacity: 0.5;cursor: var(--cursor_disabled);
}
</style>

item.vue

<template><div @click="checkRadio" class="radio"><spanclass="radio_arrow":class="{radio_arrow_check: radioValue === value,radio_arrow_check_disabled: radio_disabled,}"></span><span class="radio_label" :title="slotText"><slot>option</slot></span></div>
</template><script setup>
import { ref, inject, useSlots } from "vue";
const props = defineProps({value: {type: [String, Number, Boolean],default: "",},
});
const { radioValue, updateRadioValue, radio_disabled } = inject("radioValue");
const cursorStyle = radio_disabled.value? "var(--cursor_disabled)": "var(--cursor_pointer)";
const checkRadio = () => {if (radio_disabled.value) return;updateRadioValue(props.value);
};
// 设置过长文本提示词
const slots = useSlots();
const slot = slots.default ? slots.default() : undefined;
const slotText = ref("");
if (slot) {slotText.value = slot[0].children;
} else {slotText.value = "option";
}
</script>
<style scoped>
@keyframes blink {0% {opacity: 1;}50% {opacity: 0;}
}.radio {display: flex;justify-content: center;align-items: center;gap: 8px;cursor: v-bind(cursorStyle);
}
.radio_arrow {width: 16px;height: 16px;position: relative;
}.radio_label {max-width: 150px;white-space: nowrap; /* 确保文本在一行内显示 */overflow: hidden; /* 隐藏溢出的内容 */text-overflow: ellipsis; /* 使用省略号表示文本溢出 */
}
.radio_arrow_check::before {position: absolute;top: 0;left: 0;transform: translateY(-50%);content: "";width: 2px;height: 2px;color: #212529;box-shadow: 2px 2px, 4px 2px, 2px 4px, 4px 4px, 6px 4px, 8px 4px, 2px 6px,4px 6px, 6px 6px, 8px 6px, 10px 6px, 2px 8px, 4px 8px, 6px 8px, 8px 8px,10px 8px, 12px 8px, 2px 10px, 4px 10px, 6px 10px, 8px 10px, 10px 10px,2px 12px, 4px 12px, 6px 12px, 8px 12px, 2px 14px, 4px 14px;animation: blink 1s infinite steps(1);
}
.radio_arrow_check_disabled::before {animation: none;
}
</style>

7. 组件调用方式

      <p-radio-group v-model="radio" @change="changeRadio"><p-radio :value="true"></p-radio><p-radio :value="false"></p-radio></p-radio-group><p-radio-group v-model="radio" disabled><p-radio :value="true">YES</p-radio><p-radio :value="false">NO</p-radio><p-radio value="or"></p-radio></p-radio-group>

总结

至此一个完整的像素风单选组件radio就开发完成了。
本篇主要强化理解了几个点:
- vue的插槽以及插槽的嵌套使用。
- vue默认单个变量双向绑定的传值方法。
- vue的依赖注入provide和inject。
- vue的useSlots方法,用于查询slot内容。
- 组件封装逻辑。
- animation的animation-timing-function
- box-shadow的妙用。

再接再厉~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/146944.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

MAE 模型

masked autoencoders (MAE) 论文地址&#xff1a;https://arxiv.org/abs/2111.06377 代码地址&#xff1a;https://github.com/facebookresearch/mae 模型结构图: 思想&#xff1a;自监督学习&#xff08;Self-Supervised Learning&#xff09;&#xff0c;遮住大部分&…

机器学习(1)sklearn的介绍和六个主要模块、估计器、模型持久化

文章目录 1.sklearn介绍2.sklearn的模块3.监督学习和无监督学习1. 监督学习 (Supervised Learning)例子 2. 无监督学习 (Unsupervised Learning)例子 4.估计器估计器的主要特性和方法包括&#xff1a;估计器的类型&#xff1a;示例&#xff1a;使用 scikit-learn 中的估计器 5.…

恶意windows程序

Lab07-01.exe分析&#xff08;DOS攻击&#xff09; 1.当计算机重启后&#xff0c;这个程序如何确保它继续运行(达到持久化驻留)? 创建Malservice服务实现持久化 先分析sub_401040桉函数 尝试获取名为HGL345互斥量句柄&#xff0c;如果不存在则直接结束流程&#xff1b;如果存…

Zotero(7.0.5)+123云盘同步空间+Z-library=无限存储文献pdf/epub电子书等资料

选择123云盘作为存储介质的原因 原因1&#xff1a; zotero个人免费空间大小&#xff1a;300M&#xff0c;如果zotero云端也保存文献pdf资料则远远不够 原因2&#xff1a; 百度网盘同步文件空间大小&#xff1a;1G123云盘同步文件空间大小&#xff1a;10G 第一台电脑实施步骤…

23章 排序

1.编写程序&#xff0c;分别使用Comparable和Comparator接口对元素冒泡排序。 import java.util.Comparator;public class MySort {public static <E extends Comparable<E>> void bubbleSort(E[] list) {boolean needNextPass true;for (int i 1; needNextPass…

困扰霍金和蔡磊等人的渐冻症,能否在医学AI领域寻找到下一个解决方案?|个人观点·24-09-22

小罗碎碎念 前沿探索&#xff1a;医学AI在渐冻症&#xff08;Amyotrophic Lateral Sclerosis&#xff0c;ALS&#xff09;领域的研究进展 老粉都知道&#xff0c;小罗是研究肿瘤的&#xff0c;之前的推文也几乎都是探索医学AI在肿瘤领域的研究进展。 在查阅资料的时候&#xf…

跟着问题学12——GRU详解

1 GRU 1. 什么是GRU GRU&#xff08;Gate Recurrent Unit&#xff09;是循环神经网络&#xff08;Recurrent Neural Network, RNN&#xff09;的一种。和LSTM&#xff08;Long-Short Term Memory&#xff09;一样&#xff0c;也是为了解决长期记忆 和反向传播中的梯度等问题…

设计模式之结构型模式例题

答案&#xff1a;A 知识点 创建型 结构型 行为型模式 工厂方法模式 抽象工厂模式 原型模式 单例模式 构建器模式 适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 模板方法模式 职责链模式 命令模式 迭代器模式 中介者模式 解释器模式 备忘录模式 观…

如何在jupyter notebook中使用虚拟环境

一&#xff1a;在cmd中打开已经创建好的虚拟环境 二&#xff1a;安装ipykernel conda install ipykernel 三&#xff1a;安装牛逼conda conda install -c conda-forge nb_conda 四&#xff1a;运行jupyter notebook,选择虚拟环境

带你0到1之QT编程:十七、Http协议实战,实现一个简单服务器和一个客户端进行http协议通信

此为QT编程的第十七谈&#xff01;关注我&#xff0c;带你快速学习QT编程的学习路线&#xff01; 每一篇的技术点都是很很重要&#xff01;很重要&#xff01;很重要&#xff01;但不冗余&#xff01; 我们通常采取总-分-总和生活化的讲解方式来阐述一个知识点&#xff01; …

SSM+vue音乐播放器管理系统

音乐播放器管理系统 随着社会的发展&#xff0c;计算机的优势和普及使得音乐播放器管理系统的开发成为必需。音乐播放器管理系统主要是借助计算机&#xff0c;通过对首页、音乐推荐、付费音乐、论坛信息、个人中心、后台管理等信息进行管理。减少管理员的工作&#xff0c;同时…

基于微信小程序的家教信息管理系统的设计与实现(论文+源码)_kaic

摘 要 随着互联网时代的来临&#xff0c;使得传统的家教模式已不复存在&#xff0c;亟需一种方便、快捷的在线教学平台。因此&#xff0c;利用Java语言作为支撑和MySQL数据库存储数据&#xff0c;结合微信小程序的便利性&#xff0c;为用户开发出了一个更加人性化、方便的家庭…

uniapp 整合 OpenLayer3

安装openLayer插件 命令行&#xff1a;npm install ol 安装sass插件 命令行&#xff1a;npm install -D sass 使用方法&#xff1a; *** *** <style scoped lang"scss"> </style> 安装ElementPlus 命令行&#xff1a;npm install element-plus -…

【宝藏案例篇!】不在同一局域网怎么远程桌面?实现远程桌面访问的3种方法推荐

不在同一局域网怎么远程桌面&#xff1f;当两台电脑不在同一局域网时&#xff0c;实现远程桌面访问可以通过多种方法。 以下是三种推荐的方法&#xff0c;以及每种方法的详细步骤和注意事项&#xff1a; 方法一&#xff1a;使用第三方远程控制软件 选择一款可靠的第三方远程控…

18938 汉诺塔问题

### 思路 1. **递归解决问题**&#xff1a;使用递归方法解决汉诺塔问题。 2. **递归基准**&#xff1a;当只有一个盘子时&#xff0c;直接从源杆移动到目标杆。 3. **递归步骤**&#xff1a; - 将n-1个盘子从源杆移动到辅助杆。 - 将第n个盘子从源杆移动到目标杆。 - …

JavaScript二进制浮点数和四舍五入错误

二进制浮点数和四舍五入错误 实数有无数个&#xff0c;但JS通过浮点数的形式&#xff0c;只能表示有限个数&#xff0c;JS表现的常常是真实值的近似表示。 二进制无法表示类似于0.1这样的十进制数字&#xff0c;只能机器近似于0.1&#xff0c;看如下代码&#xff1a; <!D…

Python 中的方法解析顺序(MRO)

在 Python 中&#xff0c;MRO&#xff08;Method Resolution Order&#xff0c;方法解析顺序&#xff09;是指类继承体系中&#xff0c;Python 如何确定在调用方法时的解析顺序。MRO 决定了在多继承环境下&#xff0c;Python 如何寻找方法或属性&#xff0c;即它会根据一定规则…

二,MyBatis -Plus 关于映射 Java Bean 对象的注意事项和细节(详细说明)

二&#xff0c;MyBatis -Plus 关于映射 Java Bean 对象的注意事项和细节(详细说明) 文章目录 二&#xff0c;MyBatis -Plus 关于映射 Java Bean 对象的注意事项和细节(详细说明)1. 映射2. 表的映射3. 字段映射4. 字段失效5. 视图属性6. 总结&#xff1a;7. 最后&#xff1a; 1.…

【数据优化】基于GEE填补遥感缺失数据

GEE填补遥感数据缺失 1.写在前面2.填充代码2.1 年内中值数据填充MODIS NPP空值2.2 年内中值数据填充Landsat8 NDVI空值 1.写在前面 在遥感影像分析中&#xff0c;我们经常会遇到由于云层遮挡、传感器故障等多重因素导致的图像数据缺失问题。为了解决这一挑战&#xff0c;常用的…

Selenium with Python学习笔记整理(网课+网站持续更新)

本篇是根据学习网站和网课结合自己做的学习笔记&#xff0c;后续会一边学习一边补齐和整理笔记 官方学习网站在这获取&#xff1a; https://selenium-python.readthedocs.io/getting-started.html#simple-usage WEB UI自动化环境配置 (推荐靠谱的博客文章来进行环境配置,具…