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

news2024/11/15 4:19:36

目录

  • 引言
  • 做之前先仔细看看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">
    <span
      class="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">
    <span
      class="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">
    <span
      class="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.coloradmin.cn/o/2156884.html

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

相关文章

【测试】——JUnit

&#x1f4d6; 前言&#xff1a;JUnit 是一个流行的 Java 测试框架&#xff0c;主要用于编写和运行单元测试&#xff0c;用来管理测试用例。本文采用JUnit 5 目录 &#x1f552; 1. 添加依赖&#x1f552; 2. 注解&#x1f558; 2.1 Test&#x1f558; 2.2 BeforeAll AfterAll&…

【Docker】基于docker compose部署artifactory-cpp-ce服务

基于docker compose部署artifactory-cpp-ce服务 1 环境准备2 必要文件创建与编写3 拉取镜像-创建容器并后台运行4 访问JFog Artifactory 服务 1 环境准备 docker 以及其插件docker compose &#xff0c;我使用的版本如下图所示&#xff1a; postgresql 的jdbc驱动, 我使用的是…

【图像检索】基于纹理(LBP)和形状特征的图像检索,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于纹理(LBP)和形状特征&#xff08;hu特征&#xff09;的图像检索&#xff0c;用m…

力扣206.反转链表

力扣《反转链表》系列文章目录 刷题次序&#xff0c;由易到难&#xff0c;一次刷通&#xff01;&#xff01;&#xff01; 题目题解206. 反转链表反转链表的全部 题解192. 反转链表 II反转链表的指定段 题解224. 两两交换链表中的节点两个一组反转链表 题解325. K 个一组翻转…

【二等奖论文】2024年华为杯研究生数学建模E题成品论文获取入口

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片链接&#xff0c;那是获取资料的入口&#xff01; 点击链接获取【2024华为杯研赛资料汇总】&#xff1a; https://qm.qq.com/q/Wgk64ntZCihttps://qm.qq.com/q/Wgk64ntZCi 详细建模思路&#xff1a; 要解…

C++--模板(template)详解—— 函数模板与类模板

目录 1.泛型编程 2.函数模板 2.1 函数模板概念 2.2 函数模板格式 2.3 函数模板的原理 2.4 函数模板的实例化 2.5 模板参数的匹配原则 3.类模板 3.1 类模板的定义格式 3.2 类模板的实例化 1.泛型编程 在C中如果让你写一个交换函数&#xff0c;应该怎么做呢&#xff1f…

二叉树进阶【c++实现】【二叉搜索树的实现】

目录 二叉树进阶1.二叉搜索树1.1二叉搜索树的实现1.1.1二叉搜索树的查找1.1.2二叉搜索树的插入1.1.3中序遍历(排序)1.1.4二叉搜索树的删除(重点) 1.2二叉搜索树的应用1.2.1K模型1.2.2KV模型 1.3二叉搜索树的性能分析 二叉树进阶 前言&#xff1a; map和set特性需要先铺垫二叉搜…

Python3网络爬虫开发实战(16)分布式爬虫(第一版)

文章目录 一、分布式爬虫原理1.1 分布式爬虫架构1.2 维护爬取队列1.3 怎样来去重1.4 防止中断1.5 架构实现 二、Scrapy-Redis 源码解析2.1 获取源码2.2 爬取队列2.3 去重过滤2.4 调度器 三、Scrapy 分布式实现3.1 准备工作3.2 搭建 Redis 服务器3.3 部署代理池和 Cookies 池3.4…

超越sora,最新文生视频CogVideoX-5b模型分享

CogVideoX-5B是由智谱 AI 开源的一款先进的文本到视频生成模型&#xff0c;它是 CogVideoX 系列中的更大尺寸版本&#xff0c;旨在提供更高质量的视频生成效果。 CogVideoX-5B 采用了 3D 因果变分自编码器&#xff08;3D causal VAE&#xff09;技术&#xff0c;通过在空间和时…

【OpenAI o1背后技术】Sef-play RL:LLM通过博弈实现进化

【OpenAI o1背后技术】Sef-play RL&#xff1a;LLM通过博弈实现进化 OpenAI o1是经过强化学习训练来执行复杂推理任务的新型语言模型。特点就是&#xff0c;o1在回答之前会思考——它可以在响应用户之前产生一个很长的内部思维链。也就是该模型在作出反应之前&#xff0c;需要…

简单题104. 二叉树的最大深度 (python)20240922

问题描述&#xff1a; python&#xff1a; # Definition for a binary tree node. # class TreeNode(object): # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right class Solution(object…

Python 入门(一、使用 VSCode 开发 Python 环境搭建)

Python 入门第一课 &#xff0c;环境搭建...... by 矜辰所致前言 现在不会 Python &#xff0c;好像不那么合适&#xff0c;咱先不求精通&#xff0c;但也不能不会&#xff0c;话不多说&#xff0c;开干&#xff01; 这是 Python 入门第一课&#xff0c;当然是做好准备工作&a…

论前端框架的对比和选择 依据 前端框架的误区

前端框架的对比和选择依据 在前端开发中&#xff0c;有多种框架可供选择&#xff0c;以下是一些常见前端框架的对比和选择依据&#xff1a; 一、Vue.js 特点&#xff1a; 渐进式框架&#xff0c;灵活度高&#xff0c;可以逐步引入到项目中。学习曲线相对较平缓&#xff0c;容…

Java项目实战II基于Java+Spring Boot+MySQL的民宿在线预定平台(开发文档+源码+数据库)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在旅游市场…

强大的重命名工具 | Bulk Rename Utility v4.0 便携版

软件简介 Bulk Rename Utility是一款功能强大且易于使用的文件批量重命名工具。它不仅体积小巧&#xff0c;而且完全免费&#xff0c;提供了友好的用户界面。该软件允许用户对文件或文件夹进行批量重命名&#xff0c;支持递归操作&#xff0c;即包含子文件夹的重命名。 软件特…

Apache Iceberg 概述

Apache Iceberg概述 一、what is Apache Iceberg&#xff1f; 为了解决数据存储和计算引擎之间的适配的问题&#xff0c;Netflix开发了Iceberg&#xff0c;2018年11月16日进入Apache孵化器&#xff0c;2020 年5月19日从孵化器毕业&#xff0c;成为Apache的顶级项目。 Apache…

SpringBoot实战(三十)发送HTTP/HTTPS请求的五种实现方式【下篇】(Okhttp3、RestTemplate、Hutool)

目录 一、五种实现方式对比结果二、Demo接口地址实现方式三、Okhttp3 库实现3.1 简介3.2 Maven依赖3.3 配置文件3.4 配置类3.5 工具类3.6 示例代码3.7 执行结果实现方式四、Spring 的 RestTemplate 实现4.1 简介4.2 Maven依赖4.3 配置文件4.4 配置类4.5 HttpClient 和 RestTemp…

华为HarmonyOS灵活高效的消息推送服务(Push Kit) - 5 发送通知消息

场景介绍 通知消息通过Push Kit通道直接下发&#xff0c;可在终端设备的通知中心、锁屏、横幅等展示&#xff0c;用户点击后拉起应用。您可以通过设置通知消息样式来吸引用户。 开通权益 Push Kit根据消息内容&#xff0c;将通知消息分类为服务与通讯、资讯营销两大类别&…

idea2021git从dev分支合并到主分支master

1、新建分支 新建一个名称为dev的分支&#xff0c;切换到该分支下面&#xff0c;输入新内容 提交代码到dev分支的仓库 2、切换分支 切换到主分支&#xff0c;因为刚刚提交的分支在dev环境&#xff0c;所以master是没有 3、合并分支 点击push&#xff0c;将dev里面的代码合并到…

Spring AI Alibaba,阿里的AI Java 开发框架

源码地址 https://github.com/alibaba/spring-ai-alibaba