2026-03-29
笔记
0
请注意,本文编写于 64 天前,最后修改于 2 天前,其中某些信息可能已经过时。

目录

  1. 指令核心逻辑:监听元素的 mouseenter/mouseleave 事件,动态计算边界位置并添加对应样式类;
  2. 样式抽离:将 tooltip 样式封装为独立的 CSS 片段,全局引入即可;
  3. 指令钩子适配:分别适配 Vue2 和 Vue3 的指令钩子生命周期。

一、Vue2 版本封装

1. 指令文件(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; } };

2. 全局注册指令(main.js

javascript
import 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) });

二、Vue3 版本封装

1. 指令文件(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; } };

2. 全局注册指令(main.js

javascript
import { 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%); }

四、使用示例(Vue2/Vue3 通用)

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 版本
挂载钩子insertedmounted
解绑钩子unbindunmounted
注册方式Vue.directive()app.directive()
核心逻辑完全一致(边界检测/样式)完全一致(边界检测/样式)

总结

  1. 核心功能:两个版本都实现了「无箭头 tooltip + 自动边界检测 + 方向切换」,样式和交互完全一致;
  2. 使用方式:只需在元素上添加 v-tooltip="提示文本" 即可使用,支持静态/动态文本;
  3. 扩展建议
    • 可在指令中添加配置项(比如自定义间距、背景色),通过 binding.modifiersbinding.arg 实现;
    • 样式可按需修改 tooltip.css 中的间距(8px)、背景色、动画时长等;
    • 如需兼容低版本浏览器,可添加 @supports 降级处理或 polyfill。

你可以直接将对应版本的代码复制到项目中,引入样式后即可全局使用 v-tooltip 指令,无需重复编写 tooltip 逻辑。

本文作者:lantz

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!