当前位置: 首页 > article >正文

Vue3+Ts封装ToolTip组件(2.0版本)

本组件支持hover和click两种触发方式,需要更多的触发方式,可自行去扩展!!!

 

1.传递三个参数:

  • content:要展示的文本
  • position:文本出现的位置("top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "right")

  • trigger:触发的方式("hover" | "click")

  • appendToBody:是否添加到body上去

2.使用方式:

<ToolTipcontent="测试ToolTip":appendToBody="true"trigger="click"position="top"
><span class="key-word">测试ToolTip</span>
</ToolTip>

3.封装的ToolTip组件详细代码如下: 

<template><divclass="tooltip-container":class="{ 'tooltip-click': props.trigger === 'click' }"@mouseover="handleMouseOver"@mouseout="handleMouseOut"@click="handleClick"ref="triggerEl"><slot></slot><teleport to="body" :disabled="!props.appendToBody"><transition name="tooltip"><div v-if="isTooltipVisible" :class="tooltipClass" :style="tooltipStyle" ref="tooltipEl"><div class="tooltip-inner">{{ props.content }}</div></div></transition></teleport></div>
</template><script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, CSSProperties } from "vue";// 定义提示框可能出现的位置选项
const positionOptions = ["top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "right"] as const;
// 定义位置和触发方式的类型
type Position = (typeof positionOptions)[number];
type Trigger = ["hover" | "click"][number];
const defaultTrigger: Trigger = "hover";// 定义组件的 props 接口
interface TooltipProps {content: string; // 提示框内容position?: Position; // 提示框位置trigger?: Trigger; // 触发方式appendToBody?: boolean; // 是否将提示框添加到 body 中
}// 设置 props 的默认值
const props = withDefaults(defineProps<TooltipProps>(), {position: "top",trigger: defaultTrigger,appendToBody: false
});// 创建响应式引用
const triggerEl = ref<HTMLElement | null>(null); // 触发元素引用
const tooltipEl = ref<HTMLElement | null>(null); // 提示框元素引用
const isTooltipVisible = ref(false); // 提示框是否可见
const tooltipPosition = ref({ top: "0px", left: "0px" }); // 提示框位置// 处理点击外部事件
const handleClickOutside = (event: MouseEvent) => {if (props.trigger === "click" && isTooltipVisible.value) {const target = event.target as Node;if (triggerEl.value && tooltipEl.value && !triggerEl.value.contains(target) && !tooltipEl.value.contains(target)) {hideTooltip();}}
};// 更新提示框位置的函数
const updatePosition = () => {if (!triggerEl.value || !tooltipEl.value || !props.appendToBody) return;// 获取各种位置和尺寸信息const triggerRect = triggerEl.value.getBoundingClientRect();const tooltipRect = tooltipEl.value.getBoundingClientRect();const scrollTop = window.pageYOffset || document.documentElement.scrollTop;const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;const viewportWidth = window.innerWidth;const viewportHeight = window.innerHeight;let top = 0;let left = 0;const gap = 4; // 设置间隙// 计算提示框位置的核心函数const calculatePosition = () => {switch (props.position) {// 处理顶部位置的情况(top, top-start, top-end 三种)case "top":case "top-start":case "top-end": {// 检查顶部空间是否足够放置提示框(触发元素顶部位置是否大于提示框高度+间隙)if (triggerRect.top > tooltipRect.height + gap) {// 设置提示框的垂直位置:触发元素顶部位置 - 提示框高度 - 间隙top = triggerRect.top + scrollTop - tooltipRect.height - gap;// 根据不同的顶部对齐方式计算水平位置if (props.position === "top") {// top:水平居中对齐left =triggerRect.left + // 触发元素的左边界scrollLeft + // 加上页面水平滚动距离(triggerRect.width - tooltipRect.width) / 2; // 居中对齐的偏移量} else if (props.position === "top-start") {// top-start:左对齐left = triggerRect.left + scrollLeft; // 直接与触发元素左边界对齐} else {// top-end:右对齐left =triggerRect.left + // 触发元素的左边界scrollLeft + // 加上页面水平滚动距离triggerRect.width - // 加上触发元素的宽度tooltipRect.width; // 减去提示框宽度,实现右对齐}} else {// 如果顶部空间不足,自动切换到底部显示top = triggerRect.bottom + scrollTop + gap; // 设置到触发元素底部left = calculateHorizontalPosition(); // 重新计算水平位置tooltipEl.value?.classList.remove(props.position); // 移除原有位置类名tooltipEl.value?.classList.add("bottom"); // 添加底部位置类名}break;}// 处理底部位置的情况(bottom, bottom-start, bottom-end 三种)case "bottom":case "bottom-start":case "bottom-end": {// 设置提示框的垂直位置:触发元素底部 + 间隙top = triggerRect.bottom + scrollTop + gap;// 根据不同的底部对齐方式计算水平位置if (props.position === "bottom") {// bottom:水平居中对齐left =triggerRect.left + // 触发元素的左边界scrollLeft + // 加上页面水平滚动距离(triggerRect.width - tooltipRect.width) / 2; // 居中对齐的偏移量} else if (props.position === "bottom-start") {// bottom-start:左对齐left = triggerRect.left + scrollLeft; // 直接与触发元素左边界对齐} else {// bottom-end:右对齐left =triggerRect.left + // 触发元素的左边界scrollLeft + // 加上页面水平滚动距离triggerRect.width - // 加上触发元素的宽度tooltipRect.width; // 减去提示框宽度,实现右对齐}break;}// 处理左侧位置的情况case "left": {const arrowWidth = 18; // 箭头的宽度// 检查左侧空间是否足够(触发元素左侧位置是否大于提示框宽度+间隙+箭头宽度)if (triggerRect.left > tooltipRect.width + gap + arrowWidth) {// 设置水平位置:触发元素左侧 - 提示框宽度 - 间隙 - 箭头宽度left = triggerRect.left + scrollLeft - tooltipRect.width - gap - arrowWidth;// 垂直居中对齐top = triggerRect.top + scrollTop + (triggerRect.height - tooltipRect.height) / 2;} else {// 如果左侧空间不足,自动切换到右侧显示left = triggerRect.right + scrollLeft + gap; // 设置到触发元素右侧// 保持垂直居中top = triggerRect.top + scrollTop + (triggerRect.height - tooltipRect.height) / 2;tooltipEl.value?.classList.remove("left"); // 移除左侧位置类名tooltipEl.value?.classList.add("right"); // 添加右侧位置类名}break;}// 处理右侧位置的情况case "right": {const arrowWidth = 18; // 箭头的宽度// 检查右侧空间是否足够(触发元素右侧位置+提示框宽度+间隙是否小于视口宽度)if (triggerRect.right + tooltipRect.width + gap <= viewportWidth) {// 设置水平位置:触发元素右侧 + 间隙left = triggerRect.right + scrollLeft + gap;// 垂直居中对齐top = triggerRect.top + scrollTop + (triggerRect.height - tooltipRect.height) / 2;} else {// 如果右侧空间不足,自动切换到左侧显示// 确保左侧位置不小于间隙值left = Math.max(gap, triggerRect.left + scrollLeft - tooltipRect.width - gap - arrowWidth);// 保持垂直居中top = triggerRect.top + scrollTop + (triggerRect.height - tooltipRect.height) / 2;tooltipEl.value?.classList.remove("right"); // 移除右侧位置类名tooltipEl.value?.classList.add("left"); // 添加左侧位置类名}break;}}};// 计算水平位置,确保提示框在视口内const calculateHorizontalPosition = () => {let calculatedLeft = triggerRect.left + scrollLeft + (triggerRect.width - tooltipRect.width) / 2;if (calculatedLeft < 0) {calculatedLeft = gap;}if (calculatedLeft + tooltipRect.width > viewportWidth) {calculatedLeft = viewportWidth - tooltipRect.width - gap;}return calculatedLeft;};calculatePosition();// 确保提示框在视口范围内if (top < scrollTop) {top = scrollTop + gap;} else if (top + tooltipRect.height > scrollTop + viewportHeight) {top = scrollTop + viewportHeight - tooltipRect.height - gap;}left = Math.max(gap, Math.min(left, viewportWidth - tooltipRect.width - gap));// 更新提示框位置tooltipPosition.value = {top: `${Math.round(top)}px`,left: `${Math.round(left)}px`};
};// 显示提示框
const showTooltip = () => {isTooltipVisible.value = true;setTimeout(updatePosition, 0);
};// 隐藏提示框
const hideTooltip = () => {isTooltipVisible.value = false;
};// 处理鼠标移入事件
const handleMouseOver = () => {if (props.trigger === "hover") {showTooltip();}
};// 处理鼠标移出事件
const handleMouseOut = () => {if (props.trigger === "hover") {hideTooltip();}
};// 处理点击事件
const handleClick = () => {if (props.trigger === "click") {isTooltipVisible.value = !isTooltipVisible.value;if (isTooltipVisible.value) {setTimeout(updatePosition, 0);}}
};// 组件挂载时添加事件监听
onMounted(() => {window.addEventListener("scroll", updatePosition);window.addEventListener("resize", updatePosition);document.addEventListener("click", handleClickOutside);
});// 组件卸载前移除事件监听
onBeforeUnmount(() => {window.removeEventListener("scroll", updatePosition);window.removeEventListener("resize", updatePosition);document.removeEventListener("click", handleClickOutside);
});// 监听触发方式的变化
watch(() => props.trigger,newTrigger => {if (newTrigger === "click" && isTooltipVisible.value) {hideTooltip();}},{ immediate: true }
);// 计算提示框的 class
const tooltipClass = computed(() => {return `tooltip-content ${props.position} ${isTooltipVisible.value ? "active" : ""}`;
});// 计算提示框的样式
const tooltipStyle = computed<CSSProperties>(() => {if (!props.appendToBody) return {};return {position: "fixed",top: tooltipPosition.value.top,left: tooltipPosition.value.left,zIndex: 9999};
});
</script><style lang="scss" scoped>
@use "sass:math";
$basicW: 6px;
$arrowSize: 6px;
$backgroundColor: #454545;.tooltip-container {width: 100%;position: relative;display: inline-block;z-index: 1;
}.tooltip-content {position: absolute;z-index: 9999;pointer-events: none;.tooltip-inner {position: relative;background-color: #454545;color: #fff;padding: 8px 12px;border-radius: 4px;font-size: 14px;line-height: 1.4;white-space: normal;min-width: max-content;max-width: 300px;width: auto;word-wrap: break-word;box-shadow: 2px 2px 8px rgb(0 0 0);&::before {position: absolute;content: "";width: 0;height: 0;border: $arrowSize solid transparent;}}&.active {pointer-events: auto;}&.top,&.top-start,&.top-end {padding-bottom: $arrowSize;.tooltip-inner::before {bottom: -$arrowSize * 2;border-top-color: $backgroundColor;}}&.bottom,&.bottom-start,&.bottom-end {padding-top: $arrowSize;.tooltip-inner::before {top: -$arrowSize * 2;border-bottom-color: $backgroundColor;}}&.left {padding-right: $arrowSize;.tooltip-inner::before {right: -$arrowSize * 2;border-left-color: $backgroundColor;}}&.right {padding-left: $arrowSize;.tooltip-inner::before {left: -$arrowSize * 2;border-right-color: $backgroundColor;}}&.top,&.bottom {.tooltip-inner::before {left: 50%;transform: translateX(-50%);}}&.top-start,&.bottom-start {.tooltip-inner::before {left: $arrowSize;}}&.top-end,&.bottom-end {.tooltip-inner::before {right: $arrowSize;}}&.left,&.right {.tooltip-inner::before {top: 50%;transform: translateY(-50%);}}
}// 动画相关样式
.tooltip-enter-active,
.tooltip-leave-active {transition: opacity 0.2s ease, transform 0.2s ease-out;
}.tooltip-enter-from,
.tooltip-leave-to {opacity: 0;transform: scale(0.95);
}.tooltip-enter-to,
.tooltip-leave-from {opacity: 1;transform: scale(1);
}
</style>

相关文章:

Vue3+Ts封装ToolTip组件(2.0版本)

本组件支持hover和click两种触发方式&#xff0c;需要更多的触发方式&#xff0c;可自行去扩展&#xff01;&#xff01;&#xff01; 1.传递三个参数&#xff1a; content&#xff1a;要展示的文本 position&#xff1a;文本出现的位置&#xff08;"top" | "t…...

Latex语法入门之数学公式

Latex是一种高质量的排版系统&#xff0c;尤其擅长于数学公式的排版。本文我将带大家深入了解Latex在数学公式排版中的应用。从基础的数学符号到复杂的公式布局&#xff0c;我们都会一一讲解&#xff0c;通过本文的学习&#xff0c;你将能够轻松编写出清晰、美观的数学公式&…...

shell脚本 - Linux定时温度监控-软硬件检测 - 服务器温度监控 - 写入日志

效果图 脚本 vi auto.sh (chmod x ./auto.sh) #!/bin/bash # 按照日期创建一个文件或目录 https://blog.csdn.net/shoajun_5243/article/details/83539069 datetimedate %Y%m%d-%H%M%S |cut -b1-20 dirpath/systemMonitor/$datetime file1$dirpath/sensors.log file2$dirpa…...

Linux驱动开发进阶(六)- 多线程与并发

文章目录 1、前言2、进程与线程3、内核线程4、底半步机制4.1、软中断4.2、tasklet4.3、工作队列4.3.1、普通工作项4.3.2、延时工作项4.3.3、工作队列 5、中断线程化6、进程6.1、内核进程6.2、用户空间进程 7、锁机制7.1、原子操作7.2、自旋锁7.3、信号量7.4、互斥锁7.5、comple…...

买不起了,iPhone 或涨价 40% ?

周知的原因&#xff0c;新关税对 iPhone 的打击&#xff0c;可以说非常严重。 根据 Rosenblatt Securities分析师的预测&#xff0c;若苹果完全把成本转移给消费者。 iPhone 16 标配版的价格&#xff0c;可能上涨43%。 iPhone 16 标配的价格是799美元&#xff0c;上涨43%&am…...

Axure 列表滚动:表头非常多(横向滚动方向)、分页(纵向滚动) | 基于动态面板的滚动方向和取消调整大小以适合内容两个属性进行实现

文章目录 引言I 列表滚动的操作说明see also共享原型引言 Axure RP9教程 【数据传输】(页面值传递)| 作用域 :全局变量、局部变量 https://blog.csdn.net/z929118967/article/details/147019839?spm=1001.2014.3001.5501 基于动态面板的滚动方向和取消调整大小以适合内容两…...

RBAC 权限控制:深入到按钮级别的实现

RBAC 权限控制&#xff1a;深入到按钮级别的实现 一、前端核心思路 1. 大致实现思路 后端都过SELECT连表查询把当前登录的用户对应所有的权限返回过来&#xff0c;前端把用户对应所有的权限 存起来to(vuex/pinia) 中 &#xff0c;接着前端工程师需要知道每个按钮对应的权限代…...

大模型格式化输出的几种方法

大模型格式化输出的几种方法 在开发一些和LLM相关的应用的时候,如何从大模型的反馈中拿到结构化的输出数据是非常重要的,那么本文就记录几种常用的方法。 OpenAI提供的新方法 在 OpenAI 的 Python 库中,client.beta.chat.completions.parse 是一个用于生成结构化输出的方法…...

【区间贪心】合并区间 / 无重叠区间 / 用最少数量的箭引爆气球 / 俄罗斯套娃信封问题

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;贪心算法 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 合并区间无重叠区间用最少数量的箭引爆气球俄罗斯套娃信封问题 合并区间 合并区间 class Solution { public:vector<vecto…...

JBDC java数据库连接(2)

目录 JBDC建立 获得PrepareStatement执行sql语句 形式&#xff1a; PrepareStatement中的方法: 实例 PreparedStatement和Statement 基于以下的原因: JBDC建立 获得PrepareStatement执行sql语句 在sql语句中参数位置使用占位符,使用setXX方法向sql中设置参数 形式&…...

es --- 集群数据迁移

目录 1、需求2、工具elasticdump2.1 mac安装问题解决 2.2 elasticdump文档 3、迁移 1、需求 迁移部分新集群没有的索引和数据 2、工具elasticdump Elasticdump 的工作原理是将输入发送到输出 。两者都可以是 elasticsearch URL 或 File 2.1 mac安装 前置&#xff1a;已经安装…...

Redis高频面试题及深度解析(20大核心问题+场景化答案)

摘要&#xff1a;Redis作为高性能缓存与内存数据库&#xff0c;是后端开发的核心技术栈之一。本文整理20大高频Redis面试题&#xff0c;结合真实场景与底层源码逻辑&#xff0c;助你彻底掌握Redis核心机制。涵盖单线程模型、集群方案、分布式锁、持久化等核心知识点。 一、Redi…...

事件处理程序

事件处理程序 一、事件处理程序的定义 事件处理程序是一段代码&#xff0c;用于响应特定的事件。在网页开发中&#xff0c;事件是在文档或浏览器窗口中发生的特定交互瞬间&#xff0c;如用户点击按钮、页面加载完成等。事件处理程序则是针对这些事件执行的函数&#xff0c;它能…...

stable diffusion部署ubuntu

stable-diffusion webui: https://github.com/AUTOMATIC1111/stable-diffusion-webui python3.10 -m venv venv&#xff08;3.11的下torch会慢得要死&#xff09; source venv/bin/activate 下载checkpoint模型放入clip_version"/home/chen/软件/stable-diffusion-webu…...

Qt的window注册表读写以及删除

Qt的window注册表读写以及删除 1. 使用 QSettings&#xff08;Qt推荐方式&#xff09;基本操作关键点限制 2. 调用Windows原生API示例&#xff1a;创建/读取键值常用API注意事项 3. 高级场景(1) 递归删除键(2) 注册表权限修改 4. 安全性建议总结其他QT文章推荐 在Qt中操作Windo…...

聊一聊接口测试时遇到上下游依赖时该如何测试

目录 一、手工测试时的处理方法 1.1沟通协调法 1.2模拟数据法 二、自动化测试时的处理方法 2.1 数据关联法&#xff08;变量提取&#xff09; 2.2 Mock数据法 2.3自动化框架中的依赖管理 三、实施示例&#xff08;以订单接口测试为例&#xff09; 3.1Mock依赖接口&…...

C++ 排序(1)

以下是一些插入排序的代码 1.插入排序 1.直接插入排序 // 升序 // 最坏&#xff1a;O(N^2) 逆序 // 最好&#xff1a;O(N) 顺序有序 void InsertSort(vector<int>& a, int n) {for (int i 1; i < n; i){int end i - 1;int tmp a[i];// 将tmp插入到[0,en…...

【有啥问啥】深入浅出讲解 Teacher Forcing 技术

深入浅出讲解 Teacher Forcing 技术 在序列生成任务&#xff08;例如机器翻译、文本摘要、图像字幕生成等&#xff09;中&#xff0c;循环神经网络&#xff08;RNN&#xff09;以及基于 Transformer 的模型通常采用自回归&#xff08;autoregressive&#xff09;的方式生成输出…...

zk基础—zk实现分布式功能

1.zk实现数据发布订阅 (1)发布订阅系统一般有推模式和拉模式 推模式&#xff1a;服务端主动将更新的数据发送给所有订阅的客户端。 拉模式&#xff1a;客户端主动发起请求来获取最新数据(定时轮询拉取)。 (2)zk采用了推拉相结合来实现发布订阅 首先客户端需要向服务端注册自己关…...

mySQL数据库和mongodb数据库的详细对比

以下是 MySQL 和 MongoDB 的详细对比&#xff0c;涵盖优缺点及适用场景&#xff1a; 一、核心特性对比 特性MySQL&#xff08;关系型数据库&#xff09;MongoDB&#xff08;文档型 NoSQL 数据库&#xff09;数据模型结构化表格&#xff0c;严格遵循 Schema灵活的文档模型&…...

ubuntu wifi配置(命令行版本)

1、查询当前设备环境的wifi列表 nmcli dev wifi list2、连接wifi nmcli dev wifi connect "MiFi-SSID" password "Password" #其中MiFi-SSID是wifi的密码&#xff0c;Password是wifi的密码3、查看连接情况 nmcli dev status...

Docker与Kubernetes在ZKmall开源商城容器化部署中的应用

ZKmall开源商城作为高并发电商系统&#xff0c;其容器化部署基于DockerKubernetes技术栈&#xff0c;实现了从开发到生产环境的全流程标准化与自动化。以下是核心应用场景与技术实现&#xff1a; 一、容器化基础&#xff1a;Docker镜像与微服务隔离 ​服务镜像标准化 ​分层构建…...

华为AI-agent新作:使用自然语言生成工作流

论文标题 WorkTeam: Constructing Workflows from Natural Language with Multi-Agents 论文地址 https://arxiv.org/pdf/2503.22473 作者背景 华为&#xff0c;北京大学 动机 当下AI-agent产品百花齐放&#xff0c;尽管有ReAct、MCP等框架帮助大模型调用工具&#xff0…...

MYSQL数据库语法补充

一&#xff0c;DQL基础查询 DQL&#xff08;Data Query Language&#xff09;数据查询语言&#xff0c;可以单表查询&#xff0c;也可以多表查询 语法&#xff1a; select 查询结果 from 表名 where 条件&#xff1b; 特点&#xff1a; 查询结果可以是&#xff1a;表中的字段…...

Elasticsearch单节点安装手册

Elasticsearch单节点安装手册 以下是一份 Elasticsearch 单节点搭建手册&#xff0c;适用于 Linux 系统&#xff08;如 CentOS/Ubuntu&#xff09;&#xff0c;供学习和测试环境使用。 Elasticsearch 单节点搭建手册 1. 系统要求 操作系统&#xff1a;Linux&#xff08;Cent…...

在Windows搭建gRPC C++开发环境

一、环境构建 1. CMake Download CMake 2. Git Git for Windows 3. gRPC源码 git clone -b v1.48.0 https://github.com/grpc/grpc 进入源码目录 cd grpc 下载依赖库 git submodule update --init 二、使用CMake生成工程文件 三、使用vs2019编译grpc库文件 四、使用…...

[Python] 企业内部应用接入钉钉登录,端内免登录+浏览器授权登录

[Python] 为企业网站应用接入钉钉鉴权&#xff0c;实现钉钉客户端内自动免登授权&#xff0c;浏览器中手动钉钉授权登录两种逻辑。 操作步骤 企业内部获得 开发者权限&#xff0c;没有的话先申请。 访问 钉钉开放平台-应用开发 创建一个 企业内部应用-钉钉应用。 打开应用…...

编程题学习

acwing 826. 单链表 #include <iostream>using namespace std;const int N 100010;int idx, e[N], ne[N], head;void init() {head -1;idx 0; }void insert_head(int x) {e[idx] x;ne[idx] head;head idx ; }void delete_k_pos(int x, int k) {e[idx] x;ne[idx…...

Dev C++单个源文件和项目两种编程方式介绍

Dev C单个源文件和项目两种编程方式介绍 Dev-C 是一款免费、开源的 C/C 集成开发环境&#xff08;IDE&#xff09;&#xff0c;专为初学者和中级程序员设计&#xff0c;具有简单易用、功能丰富等特点。 Dev C 支持单文件编程和项目编程两种方式。它们之间的主要区别在于如何组…...

用AbortController取消事件绑定

视频教程 React - &#x1f914; Abort Controller 到底是什么神仙玩意&#xff1f;看完这个视频你就明白了&#xff01;&#x1f4a1;_哔哩哔哩_bilibili AbortController的好处之一是事件绑定的函数已无需具名函数,匿名函数也可以被取消事件绑定了 //该代码2秒后点击失效…...