mouseenter/mouseleave 事件,动态计算边界位置并添加对应样式类;directives/tooltip.js)javascript// Vue2 自定义指令 v-tooltip
export default {
// 绑定元素插入父节点时触发
inserted(el, binding) {
// 存储 tooltip 文本
el.setAttribute('tooltip', binding.value || '默认提示');
// 边界检测并设置 tooltip 方向
const setTooltipDirection = () => {
const rect = el.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 移除所有方向类
el.classList.remove('tooltip-top', 'tooltip-bottom', 'tooltip-left', 'tooltip-right');
// 边界检测逻辑
if (rect.top < 80) {
el.classList.add('tooltip-bottom');
} else if (viewportHeight - rect.bottom < 80) {
el.classList.add('tooltip-top');
} else if (rect.left < 80) {
el.classList.add('tooltip-right');
} else if (viewportWidth - rect.right < 80) {
el.classList.add('tooltip-left');
} else {
el.classList.add('tooltip-top');
}
};
// 鼠标进入时计算方向
el.addEventListener('mouseenter', setTooltipDirection);
// 销毁时移除事件监听(Vue2 用 unbind 钩子)
el._tooltipHandler = setTooltipDirection;
},
// 更新指令值时触发(比如 tooltip 文本变化)
update(el, binding) {
el.setAttribute('tooltip', binding.value || '默认提示');
},
// 解绑时清理
unbind(el) {
el.removeEventListener('mouseenter', el._tooltipHandler);
delete el._tooltipHandler;
}
};
main.js)javascriptimport Vue from 'vue';
import tooltipDirective from './directives/tooltip';
// 引入 tooltip 样式(见下方通用样式)
import './styles/tooltip.css';
// 注册全局指令 v-tooltip
Vue.directive('tooltip', tooltipDirective);
new Vue({
el: '#app',
render: h => h(App)
});
directives/tooltip.js)javascript// Vue3 自定义指令 v-tooltip
export default {
// 绑定元素挂载到 DOM 时触发(替代 Vue2 的 inserted)
mounted(el, binding) {
// 存储 tooltip 文本
el.setAttribute('tooltip', binding.value || '默认提示');
// 边界检测并设置 tooltip 方向
const setTooltipDirection = () => {
const rect = el.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 移除所有方向类
el.classList.remove('tooltip-top', 'tooltip-bottom', 'tooltip-left', 'tooltip-right');
// 边界检测逻辑
if (rect.top < 80) {
el.classList.add('tooltip-bottom');
} else if (viewportHeight - rect.bottom < 80) {
el.classList.add('tooltip-top');
} else if (rect.left < 80) {
el.classList.add('tooltip-right');
} else if (viewportWidth - rect.right < 80) {
el.classList.add('tooltip-left');
} else {
el.classList.add('tooltip-top');
}
};
// 鼠标进入时计算方向
el.addEventListener('mouseenter', setTooltipDirection);
// 存储 handler 以便卸载时移除
el._tooltipHandler = setTooltipDirection;
},
// 更新指令值时触发
updated(el, binding) {
el.setAttribute('tooltip', binding.value || '默认提示');
},
// 解绑时清理(Vue3 用 unmounted 钩子)
unmounted(el) {
el.removeEventListener('mouseenter', el._tooltipHandler);
delete el._tooltipHandler;
}
};
main.js)javascriptimport { createApp } from 'vue';
import App from './App.vue';
import tooltipDirective from './directives/tooltip';
// 引入 tooltip 样式(见下方通用样式)
import './styles/tooltip.css';
const app = createApp(App);
// 注册全局指令 v-tooltip
app.directive('tooltip', tooltipDirective);
app.mount('#app');
styles/tooltip.css)css/* 无箭头 Tooltip 通用样式 */
[tooltip] {
position: relative;
z-index: 10;
cursor: pointer;
}
/* Tooltip 气泡样式 */
[tooltip]::before {
content: attr(tooltip);
position: absolute;
padding: 6px 10px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 14px;
opacity: 0;
white-space: nowrap;
pointer-events: none;
transform: var(--transform);
animation: tooltip-fade 0.2s ease-in forwards;
display: none;
}
/* hover 显示气泡 */
[tooltip]:hover::before {
display: block;
}
/* 透明度动画 */
@keyframes tooltip-fade {
0% {
opacity: 0;
transform: var(--transform);
}
100% {
opacity: 1;
transform: var(--transform);
}
}
/* 各方向定位 */
.tooltip-top::before {
bottom: calc(100% + 8px);
left: 50%;
--transform: translateX(-50%);
}
.tooltip-bottom::before {
top: calc(100% + 8px);
left: 50%;
--transform: translateX(-50%);
}
.tooltip-left::before {
right: calc(100% + 8px);
top: 50%;
--transform: translateY(-50%);
}
.tooltip-right::before {
left: calc(100% + 8px);
top: 50%;
--transform: translateY(-50%);
}
vue<template> <div style="display: flex; flex-wrap: wrap; justify-content: space-between; min-height: 90vh; padding: 20px;"> <!-- 基础使用 --> <button v-tooltip="'顶部边界,我往下弹'" style="align-self: flex-start;">顶部边界</button> <button v-tooltip="'底部边界,我往上弹'" style="align-self: flex-end;">底部边界</button> <button v-tooltip="'左侧边界,我往右弹'" style="align-self: center;">左侧边界</button> <button v-tooltip="'右侧边界,我往左弹'" style="align-self: center;">右侧边界</button> <button v-tooltip="'中间区域,默认往上弹'" style="align-self: center; margin: auto;">中间区域</button> <!-- 动态文本示例 --> <button v-tooltip="dynamicText" style="margin: 20px auto;">动态提示</button> </div> </template> <script> export default { // Vue2 写法 data() { return { dynamicText: '我是动态变化的提示文本' }; } // Vue3 组合式 API 写法 // setup() { // const dynamicText = ref('我是动态变化的提示文本'); // return { dynamicText }; // } }; </script>
| 对比项 | Vue2 版本 | Vue3 版本 |
|---|---|---|
| 挂载钩子 | inserted | mounted |
| 解绑钩子 | unbind | unmounted |
| 注册方式 | Vue.directive() | app.directive() |
| 核心逻辑 | 完全一致(边界检测/样式) | 完全一致(边界检测/样式) |
v-tooltip="提示文本" 即可使用,支持静态/动态文本;binding.modifiers 或 binding.arg 实现;tooltip.css 中的间距(8px)、背景色、动画时长等;@supports 降级处理或 polyfill。你可以直接将对应版本的代码复制到项目中,引入样式后即可全局使用 v-tooltip 指令,无需重复编写 tooltip 逻辑。
本文作者:lantz
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!