一个基于vue的网页图片浏览插件

3,244 阅读8分钟

为什么会有这个组件

因为某天我在用电脑逛p站(pixiv)时,发现看图的效果不是那么令人满意,点开一个图片后居然不能放大,上移下移要通过鼠标滚轮,感觉有点反人类,我希望在网页看图时能全屏浏览图片,并且支持图片的放大缩小和拖拽,不知道是不是搜索关键字不对,逛了一圈发现莫得喜欢的轮子,刚好又有些思路而且闲着无聊,于是决定自己封装一个组件。最后的效果是这样的。好耶,2K曲面屏看纸片人老婆

截图半分钟,压图半小时

你也可以点这里在线查看效果(要用电脑端哦)

源码在这里,感谢这个项目让我想起了我的github账号(求一波star)

使用方法

注册

全局注册

1. npm install easy-preview -D
2. 在main.js中加入下面两句
    import EasyPreview from "easy-preview";
    Vue.use(EasyPreview);
3. 使用<EasyPreview>标签即可

局部注册

1. 使用npm install easy-preview -D安装插件
2. 在组件中注册EasyPreview
3. 使用<EasyPreview>标签即可
import EasyPreview from "easy-preview";
export default {
  name: 'app',
  components: {EasyPreview},
  data () {
    return {}
  }
}

使用

打开和隐藏的控制权交给组件内部

比较简单,只要传入一个img标签给插槽使用并传入img-src属性的值即可

<EasyPreview :img-src="imgSrc">
    <img :src="imgSrc" width="500" style="border-radius: 10px" alt="">
</EasyPreview>

控制权不交给组件的使用

这个时候要传入一个属性options,并将options.controlByUsers置为true,此时插槽会失效,需要传入一个额外的属性:show-preview控制显示和隐藏,此时点击右上角自带的关闭按钮改为触发自定义的clickCloseButton和click-close-button 事件(两个都会触发),你可以选择监听事件并修改传入的show-preview的值。

<img :src="imgSrc" alt="" width="500" style="border-radius: 10px" @click="onclick">
<EasyPreview :img-src="imgSrc" :options="options" :show-preview="showPreview"   @clickCloseButton="onClickCloseButton"></EasyPreview>


{
    methods : { 
        onclick() {
            this.showPreview = true
        }
        onClickCloseButton() {
            this.showPreview = false;
        }
    }
}

参数说明

提供的全部可传入参数

属性名 含义 默认值 备注
imgSrc 浏览时的图片链接 ""
options 自定义选项 null 具体参数看下面
showPreview 是否展示预览图 false 仅控制权不是组件内部时生效
clickCloseButton 点击关闭按钮时会触发的自定义事件 仅控制权不是组件内部时生效 ,需要绑定回调函数
click-close-button 点击关闭按钮时会触发的自定义事件 仅控制权不是组件内部时生效 ,需要绑定回调函数

PS:clickCloseButton绑定的事件执行时会被传入一个函数,执行这个函数可以把图片恢复初始状态,调用时可以传入一个延迟执行的时间,这个时间默认是500ms(如果你没有修改transition的时间的话,最好不要修改它)

onClickCloseButton(reset) {
    this.showPreview = false;
    reset(500);
},

options的几个可选项

属性名 含义 默认值 备注
controlByUsers 控制权是否交给组件外部 false
showCloseButton 是否显示右上角的关闭按钮 true
showStatusExtraStyle 展示状态时额外的样式 "" 可以传入对象或者字符串,样式优先级为内联级
hideStatusExtraStyle 隐藏状态时额外的样式 "" 可以传入对象或者字符串,样式优先级为内联级
buttonExtraStyle 右上的按钮没有hover时的额外样式 "" 可以传入对象或者字符串,样式优先级为内联级
buttonHoverExtraStyle 右上的按钮hover时的额外样式 "" 可以传入对象或者字符串,样式优先级为内联级

实现思路

鼠标滚动缩放

鼠标滚动时先判断是上还是下,上是放大,下就是缩小,直接通过transform来放大缩小就行

放大时的处理

放大两次鼠标指着的位置(缩放中心transform-origin)不同,就要移动图片来保持放大后鼠标扔指着同个位置,就需要进行移动,移动的代码如下

this.magnification是现在的缩放倍数,this.prevOrigin.x 和 this.prevOrigin.y是上次的缩放中心

this.e 和 this.f 是图像水平和垂直方向的偏移量,对应transform: matrix(a, b, c, d, e, f) 的e和f

// 计算需要偏移的量
let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x);
let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y);
// 进行移动
this.e -= moveX;
this.f -= moveY;

当然也不是每次放大都会保证鼠标指着的位置不变,在图像视觉大小小于屏幕大小(准确来说是父容器大小,但是全屏时大小一致)时,要做到放大是两边同时等距放大,所以在代码里有下面的处理

// 如果图片的视觉大小小于wrapper的大小,就把transform-origin取到图片中央,强制等距放大
if (imgVisualHeight < wrapperHeight) {
    originY = imgHeight / 2;
}
if (imgVisualWidth < wrapperWidth) {
    originX = imgWidth / 2;
}

缩小时的处理

缩放倍率大于1.5

如果缩放倍率大于1.5,就尽可能保证缩小后鼠标扔指向相同的位置,但这并不是100%的,在缩放前要计算下次缩放后图片会不会出现图片比屏幕大,但是又有部分图片没有填充到图片的情况,代码如下

this.rate是放大或缩小一次的倍数,这里是1.05

// 如果现在的缩放倍率已经大于1.5
 if (this.magnification > 1.5) {
    // 计算让鼠标能指向同个位置的修正X和Y
    let moveX = (1 - this.magnification) * (originX - this.prevOrigin.x);
    let moveY = (1 - this.magnification) * (originY - this.prevOrigin.y);

    // 计算下次图片放缩后的位置
    const imgOffsetLeft = this.$refs.img.offsetLeft;
    const imgOffsetTop = this.$refs.img.offsetTop;
    const magnification = this.magnification / this.rate;
    const x = this.originX;
    const y = this.originY;
    const e = this.e - moveX;
    const f = this.f - moveY;
    const nextImgVisualWidth = imgWidth * magnification;
    const nextImgVisualHeight = imgHeight * magnification;
    // 图片的视觉左边 / 顶边 / 右边 / 底边离wrapper左边 / 顶边 / 右边 / 底边的距离
    const left = (magnification - 1) * x - e - imgOffsetLeft;
    const top = (magnification - 1) * y - imgOffsetTop - f;
    const right = nextImgVisualWidth - left - wrapperWidth;
    const bottom = nextImgVisualHeight - top - wrapperHeight;

计算出图片四边离容器四边的距离后,就开始处理有空白的情况,限于篇幅仅展示x轴方向的处理

// 如果缩放后的图片比wrapper的宽,同时图片左边没有顶到wrapper的左边
if (nextImgVisualWidth > wrapperWidth && left < 0) {
    // 让图片的左边能顶到wrapper的左边
    moveX -= left;
} else if (nextImgVisualWidth > wrapperWidth && right < 0) {
    // 让图片的左边能顶到wrapper的右边
    moveX += right;
}

还有另外一种情况,就是尽管缩放倍数大于1.5,但图片的宽或高的某一边仍然不能占据完屏幕,这个时候也要保证缩小后两边的间隙相同,处理在下面

// 如果图片的大小已经小于wrapper的大小
// 要让图片两边的空白相同
if (nextImgVisualWidth < wrapperWidth) {
    // 计算要移动多少才能让两边的空白相同
    let average = (left + right) / 2;
    let diff = left - average;
    // 移动过去
    moveX -= diff;
}
if (nextImgVisualHeight < wrapperHeight) {
    let average = (top + bottom) / 2;
    let diff = top - average;
    moveY -= diff;
}

计算完要偏移的量就可以偏移了

// 修正位置
this.e -= moveX;
this.f -= moveY;
缩放倍率小于1.5

这时开始逐渐恢复图片,即将e和f归0,但是不能一下子归0,所以我们用下面的公式计算这次移动的多少

// 本次移动距离 = 还需移动的长度 ÷ 还能移动的次数
let moveX = -this.e / this.optionCount;
let moveY = -this.f / this.optionCount;

这样会显得比较"循序渐进",当然移动后可能出现上面说的,图片宽高大于屏幕宽高但该方向没占满屏幕,或者图片宽高小于屏幕宽高,移动后两边的间隙不同的情况,所以还是需要进行相同的处理,因为上面已经放过代码了,这里就不再说一次了。

鼠标拖动移动图片

估计不少人都做过拖拽移动吧,这里用的也是类似的原理,在mousedown中记录坐标,在mousemove中计算偏移,然后修改x和y方向的偏移,这里要说的就是一些边缘判断

在进行真正的移动前,要计算这么移动后会不会出现图片大于屏幕,但是又出现空白的情况,即,移动距离 = Math.min(图片的边缘屏幕边缘的距离, 鼠标离上一次位置的偏移量),这样就可以保证不会越界,如果图片大小已经小于屏幕大小,就不能在对应方向上移动了。 代码如下

// 水平移动的方向
let hd = translateX > 0 ? "right" : "left";
// 垂直移动方向
let vd = translateY > 0 ? "down" : "up";

.... // 省略了计算这四个值的代码,因为我写代码时偷懒复制粘贴了
const left = (magnification - 1) * x - e - imgOffsetLeft;
const top = (magnification - 1) * y - imgOffsetTop - f;
const right = nextImgVisualWidth - left - wrapperWidth;
const bottom = nextImgVisualHeight - top - wrapperHeight;
// 判断图片能否向左 / 右 / 上 / 下 移动
// 判断的依据是往该方向移动后是否出现空隙
let leftAble = right > 0;
let rightAble = left > 0;
let upAble = bottom > 0;
let downAble = top > 0;

// 如果水平移动方向是左
if (hd === "left") {
    if (leftAble) {
        // 计算最多能偏移的大小, 超过这个大小会出现间隙
         translateX = -Math.min(Math.abs(translateX), right);
    } else {
        // 如果不能在这个方向上移动就把偏移量置为0
        translateX = 0;
    }
}
// 其他几个方向的处理同理
......

总体来说思路还是简单的,就是写的时候有点坑233

完整代码(带详细注释)点 这里

后记

为什么有这个后记呢,因为不写点啥就会显得结束地很突兀,所以就写这个后记来缓冲一下。 嗯嗯,那么下次见。