在开发中,发现element-ui在el-table中添加图片悬浮显示时,会被单元格遮挡的问题。通过查询得到的解决办法,大多是修改.el-table类中相关样式属性,但经过验证发现会影响到其他正常功能的使用。对于此问题解决其实也并不难,将悬浮图片放在body节点下,通过定位显示即可。所以对于此问题,将通过Vue.directive钩子函数,自定义弹框来实现。
一、Vue.directive
在解决上述问题前,先了解下Vue.directive构子函数相关功能。除了Vue中核心功能默认内置的指令(v-model和v-show),Vue也允许注册自己的指令。如果需要对DOM元素进行底层操作,这时就会用到自定义指令了,directive为“指令”的意思。
1.1 自定义指令对象中构子函数
一个指令定义对象中提供了几个构子函数,具体如下表:
序号 | 名称 | 描述 |
---|---|---|
1 | bind | 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 |
2 | inserted | 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 |
3 | update | 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。 |
4 | componentUpdated | 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 |
5 | unbind | 只调用一次,指令与元素解绑时调用。 |
示例代码如下:
// 注册
Vue.directive('my-directive', {
bind: function(){},
inserted: function(){},
update: function(){},
componentUpdated: function(){},
unbind: function(){}
})
除了以上方式外,如果想注册局部指令,组件中也接受一个directives的选项,代码如下:
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
1.2 指令构子函数的参数
指令钩子函数会被传入以下参数,具体如下表:
序号 | 名称 | 属性 | 描述 |
---|---|---|---|
1 | el | 指令所绑定的元素,可以用来直接操作 DOM。 | |
2 | binding | 一个对象,包含以下 property: | |
3 | name | 指令名,不包括 v- 前缀。 | |
4 | value | 指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。 | |
5 | oldValue | 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。 | |
6 | expression | 字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。 | |
7 | expression | 传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。 | |
8 | modifiers | 一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。 | |
9 | vnode | Vue 编译生成的虚拟节点。 | |
10 | oldVnode | 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。 |
注间:除了el之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建立通过元素的dataset来进行。
二、图片悬浮功能开发
这里将通过注册局部指令,来实现图片悬浮显示的功能,在组件中定义directives选项。如须全局注册,可以将以下功能移植到Vue.directive()中定义。
html中在img标签上添加v-suspended,代码如下:
<template>
<div>
<el-table size="mini" border :data="tableData">
<el-table-column type="index" label="序号" width="50px"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="图片" prop="thumb">
<template slot-scope="scope">
<div class="thumb">
<img v-if="scope.row.thumb" :src="scope.row.thumb" class="img" v-suspended />
</div>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createtime"></el-table-column>
<el-table-column label="更新时间" prop="updatetime"></el-table-column>
</el-table>
</div>
</template>
js部分代码如下:
export default {
data(){
return {
tableData: [
{name: "Angular", thumb: require("@/assets/angular.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "VueJs", thumb: require("@/assets/logo.png"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "NuxtJs", thumb: require("@/assets/nuxtjs.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "React", thumb: require("@/assets/react.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"}
]
}
},
directives: {
// 自定义悬浮v-suspended
suspended: {
bind: (el) => {
console.log('el', el);
}
}
},
// end
}
此时打开浏览器控制中,可以发现输出对应img的DOM节点,如下图:
2.1 创建悬浮框
首先,我们需要通过javascript创建一个DOM容器,用来显示悬浮图片区域,在项目目录中创建suspendedDialog.js,并引入到页面中,来实现悬浮框的创建并插入。
2.1.1 样式
这里样式通过less编写的,注意您项目中使用的css预处理器。另外需要注意的是,此弹框默认为display:none(不显示模式),只有当鼠标悬浮到对应图片上时,通过js控制其显示与隐藏。代码如下:
@width: 240px;
#suspended-dialog{
display: none;
width: @width;
min-height: @width;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
padding: 12px;
.inner{
background-color: #fff;
padding: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, .2);
border-radius: 8px;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
}
img.imgs{
width: 100%;
height: 100%;
}
}
2.1.2 SuspendedDialog类
在suspendedDialog.js文件中定义SuspendedDialog类,用于初始化图片悬浮框,以及修改悬浮框位置和显示或隐藏。代码如下:
/*
* 定义弹框类
*/
class SuspendedDialog{
constructor(){
this.idName = "suspended-dialog"; // 定义容器ID选择器名称
this.innerClassName = "inner"; // 内容器类选择器名称
this.imgClassName = "imgs"; // 图片节点类选择器名称
this.dialogWidth = 240; // 外容器宽度
this.sDialog = document.createElement('div'); // 外层容器
this.innerBox = document.createElement('div'); // 内容器对象
this.imgBox = document.createElement('img'); // 图片节点对象
}
/**
* 初始化DOM,并添加到body中
*/
initialDom(){
const sDialog = document.getElementById(this.idName); // 查询节点
// 如果节点存在,则结束后续操作
if(sDialog) return;
// 初始经属性
this.sDialog.id = this.idName;
this.innerBox.classList.add(this.innerClassName);
this.imgBox.classList.add(this.imgClassName);
// 将DOM追加到对应容器中
this.innerBox.append(this.imgBox);
this.sDialog.append(this.innerBox);
document.body.append(this.sDialog);
}
/**
* 显示与隐藏
* @param {Object} flag
* @param {Object} callback 回调函数
*/
toggle(flag, callback = () => {}){
if(flag && 'block'!=this.sDialog.style.display){
this.sDialog.style.display = 'block';
callback();
} else if(!flag && 'none'!=this.sDialog.style.display){
this.sDialog.style.display = 'none';
callback();
}
}
}
export default new SuspendedDialog();
单例模式是一种常见的设计模式,目的是确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例。
2.1.3 页面引入
页面代码如下:
import sDialog from './suspendedDialog.js'
export default {
data(){
return {
tableData: [
{name: "Angular", thumb: require("@/assets/angular.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "VueJs", thumb: require("@/assets/logo.png"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "NuxtJs", thumb: require("@/assets/nuxtjs.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"},
{name: "React", thumb: require("@/assets/react.jpg"), createtime: "2024/6/15", updatetime: "2024/6/15"}
]
}
},
directives: {
// 自定义悬浮v-suspended
suspended: {
bind: (el) => {
sDialog.initialDom();
}
}
},
// end
}
当在钩子函数中初始化弹框后,则页面中节点创建了一个单例的DOM节点。如下图:
2.2 监听事件
通过mouseenter、mouseleave事件来判断,鼠标是否经过对应图上节点或是已离开节点。 这里需要注意的是,鼠标当在图片或悬浮区域图片上时,悬浮框都正常显示,移出来隐藏。
2.2.1 修改SuspendedDialog类
此时SuspendedDialog类需要做两个修改,一是增加setImgUrl()函数,用于修改图片地址;二是增加悬浮框鼠标移入移出监听事件,用于监听悬浮框是否显示操作。代码如下:
/*
* 定义弹框类
*/
class SuspendedDialog{
constructor(){
this.idName = "suspended-dialog"; // 定义容器ID选择器名称
this.innerClassName = "inner"; // 内容器类选择器名称
this.imgClassName = "imgs"; // 图片节点类选择器名称
this.dialogWidth = 240; // 外容器宽度
this.sDialog = document.createElement('div'); // 外层容器
this.innerBox = document.createElement('div'); // 内容器对象
this.imgBox = document.createElement('img'); // 图片节点对象
}
/**
* 初始化DOM,并添加到body中
*/
initialDom(){
const sDialog = document.getElementById(this.idName); // 查询节点
// 如果节点存在,则结束后续操作
if(sDialog) return;
// 初始经属性
this.sDialog.id = this.idName;
this.innerBox.classList.add(this.innerClassName);
this.imgBox.classList.add(this.imgClassName);
// 将DOM追加到对应容器中
this.innerBox.append(this.imgBox);
this.sDialog.append(this.innerBox);
document.body.append(this.sDialog);
// 追加事件
this.addEvent();
}
/**
* 修改图片路径
* @param {Object} _url
*/
setImgUrl(_url){
this.imgBox.src = _url;
}
/**
* 添加监听事件
*/
addEvent(){
this.sDialog.addEventListener('mouseenter', e => this.toggle(true)); // 鼠标移入悬浮框区域时保持显示
this.sDialog.addEventListener('mouseleave', e => this.toggle(false)); // 鼠标移出悬浮框区域时隐藏
}
/**
* 显示与隐藏
* @param {Object} flag
* @param {Object} callback 回调函数
*/
toggle(flag, callback = () => {}){
if(flag && 'block'!=this.sDialog.style.display){
this.sDialog.style.display = 'block';
callback();
} else if(!flag && 'none'!=this.sDialog.style.display){
this.sDialog.style.display = 'none';
callback();
}
}
}
export default new SuspendedDialog();
2.2.2 页面中事件监听与图片显示
当鼠标移入图片时,先执行toggle函数显示悬浮框,当悬浮框显示后执行回调函数(只有弹框显示出来后,方可获取真实的参数数据)。在执行回调函数时,将当前鼠标所在图片的地址获取,并将其赋给悬浮框中的img节点对象。
代码如下 :
import sDialog from './suspendedDialog.js'
export default {
// ...
directives: {
// 自定义悬浮v-suspended
suspended: {
bind: (el) => {
// 初始化悬浮框
sDialog.initialDom();
// 鼠标经过图片并未移出时执行回调函数
el.addEventListener('mouseenter', function(e) {
// 显示悬浮弹框,显示后获取相应的参数信息
sDialog.toggle(true, () => {
sDialog.setImgUrl(el.src);
});
});
// 鼠标移出图片区域时,隐藏悬浮弹框
el.addEventListener('mouseleave', () => sDialog.toggle(false));
}
}
},
// end
}
运行后结果如下图:
2.3 计算悬浮框位置
如上结果可见,现在鼠标放到对应的图片上后,悬浮框可以显示对应图片信息了;但是悬浮框还未与图片进行对齐,此地则需要通过获取相应参数数据,进行计算来重新指定悬浮框位置。
2.3.1 修改SuspendedDialog类
在SuspendedDialog类中新增resetPosition()函数,用于修正悬浮弹框在新图片的位置。
示例代码如下:
/*
* 定义弹框类
*/
class SuspendedDialog{
// ...
/**
* 重新指定弹框位置
* @param {Object} boundingClientRect
*/
resetPosition(boundingClientRect){
console.log('bounding', boundingClientRect);
this.sDialog.style.top = boundingClientRect.top + "px";
this.sDialog.style.left = (boundingClientRect.width + boundingClientRect.left) + "px";
}
}
export default new SuspendedDialog();
2.3.2 页面中获取元素边界信息
当SuspendedDialog类中修正弹框位置的resetPosition()函数定义好后,页面中则可以直接调用了。而DOM元素的边界信息,通过el.getBoundingClientRect()直接获取即可。
示例代码如下:
import sDialog from './suspendedDialog.js'
export default {
// ...
directives: {
// 自定义悬浮v-suspended
suspended: {
bind: (el) => {
// 初始化悬浮框
sDialog.initialDom();
// 鼠标经过图片并未移出时执行回调函数
el.addEventListener('mouseenter', function(e) {
// 显示悬浮弹框,显示后获取相应的参数信息
sDialog.toggle(true, () => {
sDialog.resetPosition(el.getBoundingClientRect()); // 修正弹框位置
sDialog.setImgUrl(el.src); // 修改新的图片地址
});
});
// 鼠标移出图片区域时,隐藏悬浮弹框
el.addEventListener('mouseleave', () => sDialog.toggle(false));
}
}
},
// end
}
此时当鼠标放到图片上后,控制台会输出此图片元素的边界信息,如下图:
另外,悬浮框现在也可以和图片对齐显示了,如下图:
2.3.3 内填充边距
如图可见,其实悬浮弹框并未与图片进行对齐,这是由于在定义样式时,给外容器添加padding: 12px内填充边距。
右图可以清晰看出悬浮弹框三层结构,为什么这里要定义两个div容器,其目的是解决鼠标从图片区域滑到悬浮弹框区域时,中间不会现出空隙;因为鼠标一旦移出图片,悬浮框会立即隐藏掉,则不会出现鼠标在悬浮框上保持显示情况;而增加内填充,图片与悬浮框看似存在间距,但实际是保持连续性。
所以我们将内填充距离减掉里可,SuspendedDialog类再次调整,代码如下:
/*
* 定义弹框类
*/
class SuspendedDialog{
constructor(){
this.idName = "suspended-dialog"; // 定义容器ID选择器名称
this.innerClassName = "inner"; // 内容器类选择器名称
this.imgClassName = "imgs"; // 图片节点类选择器名称
this.dialogWidth = 240; // 外容器宽度
this.dialogPadding = 12; // 外容器内填充
this.sDialog = document.createElement('div'); // 外层容器
this.innerBox = document.createElement('div'); // 内容器对象
this.imgBox = document.createElement('img'); // 图片节点对象
}
// ...
/**
* 重新指定弹框位置
* @param {Object} boundingClientRect
*/
resetPosition(boundingClientRect){
this.sDialog.style.top = (boundingClientRect.top - this.dialogPadding) + "px";
this.sDialog.style.left = (boundingClientRect.width + boundingClientRect.left) + "px";
}
}
export default new SuspendedDialog();
此时如下图可见,顶部显示已对齐状态。
在实际开发中,可能会遇到下图底部超出情况,或者左侧、右侧超出情况。这里就不细讲了,对界面要求较高的朋友,可以在resetPosition()函数中,通过DOM的边界信息或其他节点数据,进行相应计算来多方位处理,使其能按您的需求展示出来。