学校网站建设的技术方案企业注册信息查询单
本组件支持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>