写在开头
(ó﹏ò。)哈喽各位,又见面了(伤心入场...ing),继 上一篇 文章点赞量惨淡收场后,小编夜不能寐,左思右想自己的不足,这次苦熬三天时间又准备了新作来留住各位的心,望能再次得到各位看官的青睐。
话不多说,咱们赶紧来开启本章的内容,这次小编给各位带来的依旧是实用类文章,分享如何开发一个完整的图片预览组件,它支持多图切换、放大缩小、旋转、鼠标滚轮操作、键盘按键控制、拖动等等的功能,并且使用方便、易扩展,零依赖。
项目初始化
项目演示技术小编采用的是 Vue2
,如果你有安装 vue-cli
脚手架的话,可以直接通过 vue create projectName
命令来初始化项目。
然后,在 components
文件夹下创建 ImagePreview
组件目录,并创建 Preview.vue
文件与 utils.js
文件,具体目录结构如下:
基本布局
以上图片是本次要实现的最终效果,我们先根据它把布局给搞定,Preview.vue
文件具体代码:
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
<div class="preview__mask"></div>
<!-- 关闭按钮 -->
<span class="preview__btn preview__close">
<svg t="1669452473618" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4022" width="48" height="48"><path d="M571.733333 512l268.8-268.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0L512 452.266667 243.2 183.466667c-17.066667-17.066667-42.666667-17.066667-59.733333 0-17.066667 17.066667-17.066667 42.666667 0 59.733333L452.266667 512 183.466667 780.8c-17.066667 17.066667-17.066667 42.666667 0 59.733333 8.533333 8.533333 19.2 12.8 29.866666 12.8s21.333333-4.266667 29.866667-12.8L512 571.733333l268.8 268.8c8.533333 8.533333 19.2 12.8 29.866667 12.8s21.333333-4.266667 29.866666-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333L571.733333 512z" p-id="4023" fill="#ffffff"></path></svg>
</span>
<!-- 图片展示区 -->
<div class="preview__canvas">
<img src="" />
</div>
<!-- 操作区 -->
<div class="preview__btn preview__actions">
<div class="actions__inner">
<svg class="actions__icon" t="1669452513061" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4278" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4279" fill="#ffffff"></path><path d="M597.333333 437.333333h-96V341.333333c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v96H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h96V597.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-96H597.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4280" fill="#ffffff"></path></svg>
<svg class="actions__icon" t="1669452664869" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4529" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4530" fill="#ffffff"></path><path d="M597.333333 437.333333H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4531" fill="#ffffff"></path></svg>
<svg class="actions__icon" t="1669452861191" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8599" width="48" height="48"><path d="M511.4 124C290.5 124.3 112 303 112 523.9c0 128 60.2 242 153.8 315.2l-37.5 48c-4.1 5.3-0.3 13 6.3 12.9l167-0.8c5.2 0 9-4.9 7.7-9.9L369.8 727c-1.6-6.5-10-8.3-14.1-3L315 776.1c-10.2-8-20-16.7-29.3-26-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-7.5 7.5-15.3 14.5-23.4 21.2-3.4 2.8-3.9 7.7-1.2 11.1l39.4 50.5c2.8 3.5 7.9 4.1 11.4 1.3C854.5 760.8 912 649.1 912 523.9c0-221.1-179.4-400.2-400.6-399.9z" p-id="8600" fill="#ffffff"></path></svg>
<svg class="actions__icon" t="1669452818298" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8349" width="48" height="48"><path d="M758.2 839.1C851.8 765.9 912 651.9 912 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1-8.1-6.6-15.9-13.7-23.4-21.2-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-9.3 9.3-19.1 18-29.3 26L668.2 724c-4.1-5.3-12.5-3.5-14.1 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 0.8c6.7 0 10.5-7.7 6.3-12.9l-37.3-47.9z" p-id="8350" fill="#ffffff"></path></svg>
</div>
</div>
<!--左右箭头-->
<span class="preview__btn preview__prev">
<svg t="1669452168825" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3772" width="48" height="48"><path d="M384 512L731.733333 202.666667c17.066667-14.933333 19.2-42.666667 4.266667-59.733334-14.933333-17.066667-42.666667-19.2-59.733333-4.266666l-384 341.333333c-10.666667 8.533333-14.933333 19.2-14.933334 32s4.266667 23.466667 14.933334 32l384 341.333333c8.533333 6.4 19.2 10.666667 27.733333 10.666667 12.8 0 23.466667-4.266667 32-14.933333 14.933333-17.066667 14.933333-44.8-4.266667-59.733334L384 512z" p-id="3773" fill="#ffffff"></path></svg>
</span>
<span class="preview__btn preview__next">
<svg t="1669452135423" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3522" width="48" height="48"><path d="M731.733333 480l-384-341.333333c-17.066667-14.933333-44.8-14.933333-59.733333 4.266666-14.933333 17.066667-14.933333 44.8 4.266667 59.733334L640 512 292.266667 821.333333c-17.066667 14.933333-19.2 42.666667-4.266667 59.733334 8.533333 8.533333 19.2 14.933333 32 14.933333 10.666667 0 19.2-4.266667 27.733333-10.666667l384-341.333333c8.533333-8.533333 14.933333-19.2 14.933334-32s-4.266667-23.466667-14.933334-32z" p-id="3523" fill="#ffffff"></path></svg>
</span>
</div>
</template>
<style scoped>
.yd-preview {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.preview__mask {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0.5;
background: #000;
}
.preview__btn {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
opacity: 0.8;
cursor: pointer;
box-sizing: border-box;
user-select: none;
}
.preview__close {
top: 40px;
right: 40px;
width: 40px;
height: 40px;
font-size: 40px;
color: #fff;
}
.preview__canvas {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview__actions {
left: 50%;
bottom: 30px;
transform: translateX(-50%);
width: 282px;
height: 44px;
padding: 0 23px;
background-color: #606266;
border-color: #fff;
border-radius: 22px;
}
.actions__inner {
width: 100%;
height: 100%;
text-align: justify;
cursor: default;
font-size: 23px;
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
align-items: center;
justify-content: space-around;
}
.actions__icon {
cursor: pointer;
width: 30px;
}
.preview__next,
.preview__prev {
top: 50%;
width: 44px;
height: 44px;
font-size: 24px;
color: #fff;
background-color: #606266;
border-color: #fff;
}
.preview__prev {
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
left: 40px;
}
.preview__next {
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
right: 40px;
text-indent: 2px;
}
.preview__prev svg{
width: 28px;
margin-right: 3px;
}
.preview__next svg {
width: 28px;
margin-left: 3px;
}
</style>
在 App.vue
文件中使用:
<template>
<div>
<Preview />
</div>
</template>
<script>
import Preview from '@/components/ImagePreview/Preview.vue';
export default {
components: { Preview },
}
</script>
相关图标可以直接上阿里巴巴的 iconfont 图库去下载即可。
多图切换
要做多图切换功能,我们需要先来准备一下图片的数据, App.vue
文件:
<template>
<div>
<Preview :url-list="imageList"/>
</div>
</template>
<script>
import Preview from '@/components/ImagePreview/Preview.vue';
export default {
components: { Preview },
data() {
return {
imageList: [
'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/191c1679754c4ad0ab8e32771968ff94~tplv-k3u1fbpfcp-watermark.image?',
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8ccef1776e0b4887bc8d47fefc4cd388~tplv-k3u1fbpfcp-watermark.image?',
'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/70dada16aa4845698f893a4d2bddcfb7~tplv-k3u1fbpfcp-watermark.image?'
]
}
},
}
</script>
多图片切换的具体逻辑在 Preview.vue
文件:
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
...
<!-- 展示区 -->
<div class="preview__canvas">
<template v-for="(url, i) in urlList">
<img
v-if="i === index"
ref="img"
:key="url"
:src="currentImg"
:style="imgStyle"
/>
</template>
</div>
<!--左右箭头-->
<span @click="prevEvent" class="preview__btn preview__prev">
<svg t="1669452168825" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3772" width="48" height="48"><path d="M384 512L731.733333 202.666667c17.066667-14.933333 19.2-42.666667 4.266667-59.733334-14.933333-17.066667-42.666667-19.2-59.733333-4.266666l-384 341.333333c-10.666667 8.533333-14.933333 19.2-14.933334 32s4.266667 23.466667 14.933334 32l384 341.333333c8.533333 6.4 19.2 10.666667 27.733333 10.666667 12.8 0 23.466667-4.266667 32-14.933333 14.933333-17.066667 14.933333-44.8-4.266667-59.733334L384 512z" p-id="3773" fill="#ffffff"></path></svg>
</span>
<span @click="nextEvent" class="preview__btn preview__next">
<svg t="1669452135423" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3522" width="48" height="48"><path d="M731.733333 480l-384-341.333333c-17.066667-14.933333-44.8-14.933333-59.733333 4.266666-14.933333 17.066667-14.933333 44.8 4.266667 59.733334L640 512 292.266667 821.333333c-17.066667 14.933333-19.2 42.666667-4.266667 59.733334 8.533333 8.533333 19.2 14.933333 32 14.933333 10.666667 0 19.2-4.266667 27.733333-10.666667l384-341.333333c8.533333-8.533333 14.933333-19.2 14.933334-32s-4.266667-23.466667-14.933334-32z" p-id="3523" fill="#ffffff"></path></svg>
</span>
...
</div>
</template>
<script>
export default {
props: {
urlList: {
type: Array,
default: () => []
},
// 初始图片的索引
initialIndex: {
type: Number,
default: 0
},
},
data() {
return {
index: this.initialIndex,
transform: {
scale: 1, // 缩放
deg: 0, // 旋转
offsetX: 0, // 拖动
offsetY: 0, // 拖动
enableTransition: false, // 过渡效果
},
infinite: true, // 是否是循环切换
}
},
computed: {
currentImg () {
return this.urlList[this.index];
},
imgStyle () {
const { scale, deg, offsetX, offsetY, enableTransition } = this.transform;
const style = {
transform: `scale(${scale}) rotate(${deg}deg)`,
transition: enableTransition ? 'transform .3s' : '',
'margin-left': `${offsetX}px`,
'margin-top': `${offsetY}px`
}
style.maxWidth = style.maxHeight = '100%';
return style;
},
isFirst () {
return this.index === 0;
},
isLast () {
return this.index === this.urlList.length - 1;
},
},
methods: {
// 上一张
prevEvent () {
if (this.isFirst && !this.infinite) return;
const len = this.urlList.length;
this.index = (this.index - 1 + len) % len;
},
// 下一张
nextEvent () {
if (this.isLast && !this.infinite) return;
const len = this.urlList.length;
this.index = (this.index + 1) % len;
},
}
}
</script>
整体代码不难,只是我们先预置了一些变量,像缩放、旋转、拖动等等,这些主要是为了后面其它功能的实现做铺垫。
然后,还有一个需要关注的地方就是"索引的界限判断",可能很多时候,切换上下一张的逻辑你会这么来写:
prevEvent () {
if (this.isFirst && !this.infinite) return;
const len = this.urlList.length;
if(this.index === 0) this.index = len - 1;
else this.index--
},
nextEvent () {
if (this.isLast && !this.infinite) return;
const len = this.urlList.length;
if(this.index === len - 1) this.index = 0;
else this.index++
},
当然,这样写完全没有问题,以前小编也经常这么写,简单易懂可读。
但是,有时你真的闲得慌的话,你也可以试试用取模(%
)的形式来算索引,用来装装13也可以嘛(✪ω✪)。
- 上一页:
this.index = (this.index - 1 + len) % len;
- 下一页:
this.index = (this.index + 1) % len;
缩放、旋转
我们接着来看看缩放与旋转是如何实现的。
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
...
<!-- 操作区 -->
<div class="preview__btn preview__actions">
<div class="actions__inner">
<svg @click.stop="handleActions('zoomIn')" class="actions__icon" t="1669452513061" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4278" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4279" fill="#ffffff"></path><path d="M597.333333 437.333333h-96V341.333333c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v96H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h96V597.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-96H597.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4280" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('zoomOut')" class="actions__icon" t="1669452664869" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4529" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4530" fill="#ffffff"></path><path d="M597.333333 437.333333H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4531" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('anticlocelise')" class="actions__icon" t="1669452861191" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8599" width="48" height="48"><path d="M511.4 124C290.5 124.3 112 303 112 523.9c0 128 60.2 242 153.8 315.2l-37.5 48c-4.1 5.3-0.3 13 6.3 12.9l167-0.8c5.2 0 9-4.9 7.7-9.9L369.8 727c-1.6-6.5-10-8.3-14.1-3L315 776.1c-10.2-8-20-16.7-29.3-26-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-7.5 7.5-15.3 14.5-23.4 21.2-3.4 2.8-3.9 7.7-1.2 11.1l39.4 50.5c2.8 3.5 7.9 4.1 11.4 1.3C854.5 760.8 912 649.1 912 523.9c0-221.1-179.4-400.2-400.6-399.9z" p-id="8600" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('clocelise')" class="actions__icon" t="1669452818298" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8349" width="48" height="48"><path d="M758.2 839.1C851.8 765.9 912 651.9 912 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1-8.1-6.6-15.9-13.7-23.4-21.2-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-9.3 9.3-19.1 18-29.3 26L668.2 724c-4.1-5.3-12.5-3.5-14.1 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 0.8c6.7 0 10.5-7.7 6.3-12.9l-37.3-47.9z" p-id="8350" fill="#ffffff"></path></svg>
</div>
</div>
...
</div>
</template>
<script>
export default {
...,
data() {
...,
loading: false,
},
methods: {
...,
// 缩放、旋转
handleActions (action, options = {}) {
if (this.loading) return
const { zoomRate, rotateDeg, enableTransition } = {
zoomRate: 0.2, // zoomRate: 每次缩放级别
rotateDeg: 90, // rotateDeg: 每次旋转角度大小
enableTransition: true,
...options
}
const { transform } = this
switch (action) {
case 'zoomOut':
if (transform.scale > 0.2) { // 限制最小缩放级别
transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3))
}
break
case 'zoomIn':
transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3))
break
case 'clocelise':
transform.deg += rotateDeg
break
case 'anticlocelise':
transform.deg -= rotateDeg
break
}
transform.enableTransition = enableTransition
},
}
}
从代码上看,缩放与旋转本质就是修改图片的样式而已,那就没有什么可以絮叨的了,我们继续往下看。
Loading效果
上面我们在 data
中增加一个 loading
属性,它是用来控制图片载入过程的加载效果,接下来我们先来把它的功能完善一下。
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
...
<!-- 展示区 -->
<div class="preview__canvas">
<template v-for="(url, i) in urlList">
<img
v-if="i === index"
ref="img"
:key="url"
:src="currentImg"
:style="imgStyle"
@load="handleImgLoad"
@error="handleImgError"
/>
</template>
<svg v-if="loading" class="preview__loading" viewBox="25 25 50 50">
<circle class="preview__loading-path" cx="50" cy="50" r="20" fill="none"/>
</svg>
</div>
...
</div>
</template>
<script>
export default {
...,
watch: {
currentImg () {
this.$nextTick(() => {
const $img = this.$refs.img[0];
// complete: 是否已完成对图像的加载; 每次切换图片complete基本都是false, 再配合load事件
if (!$img.complete) this.loading = true;
})
}
},
methods: {
...,
// 图片载入完成
handleImgLoad (e) {
this.loading = false;
},
// 图片载入失败
handleImgError (e) {
this.loading = false;
e.target.alt = '加载失败';
},
}
}
</script>
<style scoped>
...
.preview__loading {
position: absolute;
height: 50px;
width: 50px;
animation: loading-rotate 2s linear infinite;
}
.preview__loading-path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2px;
stroke: rgba(255, 255, 255, 0.8);
stroke-linecap: round;
}
@keyframes loading-rotate {
100% {
transform: rotate(360deg)
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
</style>
图片的 Loding
效果主要是通过 complete
属性与 onload
方法来实现的。
complete
属性:可返回浏览器是否已完成对图像的加载。如果加载完成,则返回true
,否则返回fasle
。onload
事件:事件在图片加载完成后立即执行。
需要我们注意的是 complete
只是 HTMLImageElement
对象的一个属性,而 onload
则是这个 Image
对象的 load
事件回调,前者不能准确的在事件发生时进行异步回调并且在浏览器的兼容性上也有些问题。
只有 img.complete
可以判断图片加载完成,img.onload
并不能判断图片是否加载完,而是在加载完毕之后,直接运行 onload
绑定的函数。
img
加载完成就会解除 onload
事件,src
是异步加载图片的,如果在绑定事件前就已经加载完成, onload
事件不会触发。 img.complete
是一直都有的属性,加载完成后为 true
。
鼠标滚轮、键盘按键事件监听
接下来需要来处理鼠标滚轮事件、以及键盘按键的事件。
我们会注册许多监听事件,为此我们先来准备一些工具方法,在 utils.js
文件:
// 判断是否是火狐
export const isFirefox = function() {
return !!window.navigator.userAgent.match(/firefox/i);
};
// 节流函数
export function rafThrottle(fn) {
let locked = false;
return function(...args) {
if (locked) return;
locked = true;
window.requestAnimationFrame(_ => {
fn.apply(this, args);
locked = false;
});
};
}
// 给DOM注册监听事件
export const on = function(element, event, handler) {
if(document.addEventListener) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
}else {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}
// 移除DOM的事件监听
export const off = function(element, event, handler) {
if (document.removeEventListener) {
if (element && event) {
element.removeEventListener(event, handler, false);
}
}else {
if (element && event) {
element.detachEvent('on' + event, handler);
}
}
}
以上这些操作主要还是调用原先方法来工作的,我们只要把对应事件注册好就可以了,具体逻辑:
<script>
import { on, off, isFirefox, rafThrottle } from './utils.js';
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel';
export default {
...,
mounted () {
this.deviceSupportInstall();
},
methods: {
...,
hideEvent() {},
// 注册鼠标滚轮与键盘按键事件
deviceSupportInstall () {
this._keyDownHandler = rafThrottle((e) => {
const keyCode = e.keyCode;
switch (keyCode) {
// ESC-关闭
case 27:
this.hideEvent();
break
// LEFT_ARROW-上一张
case 37:
this.prevEvent();
break
// UP_ARROW-放大
case 38:
this.handleActions('zoomIn');
break
// RIGHT_ARROW-下一张
case 39:
this.nextEvent();
break
// DOWN_ARROW-缩小
case 40:
this.handleActions('zoomOut');
break
}
})
this._mouseWheelHandler = rafThrottle((e) => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail;
// 判断鼠标滚轮是向上滚动还是向下滚动
if (delta > 0) {
this.handleActions('zoomIn', {
zoomRate: 0.015,
enableTransition: false
})
} else {
this.handleActions('zoomOut', {
zoomRate: 0.015,
enableTransition: false
})
}
})
on(document, 'keydown', this._keyDownHandler); // 监听键盘按键
on(document, mousewheelEventName, this._mouseWheelHandler); // 监听鼠标滚轮
},
}
}
</script>
整体代码比较简单,但要注意火狐浏览器的滚轮事件名称稍微有一点不同,需要额外判断一下。
上面的关闭事件还没写,我们来继续把它完善一下。
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
...
<!-- 关闭按钮 -->
<span class="preview__btn preview__close" @click="hideEvent">
...
</span>
...
</div>
</template>
<script>
export default {
props: {
...,
onClose: {
type: Function,
default: () => {}
}
},
methods: {
...,
hideEvent() {
this.deviceSupportUninstall();
this.onClose()
},
// 解除事件
deviceSupportUninstall () {
off(document, 'keydown', this._keyDownHandler)
off(document, mousewheelEventName, this._mouseWheelHandler)
this._keyDownHandler = null
this._mouseWheelHandler = null
},
}
}
</script>
既然注册了监听事件,那就需要注意把事件解绑,我们将这个过程放在组件关闭的时候,而且组件的关闭事件是放置在组件外部控制的,通过 props
来完成。
App.vue
文件:
<template>
<div>
<Preview :url-list="imageList" :onClose="close" v-show="showPreview" />
</div>
</template>
<script>
import Preview from '@/components/ImagePreview/Preview.vue';
export default {
components: { Preview },
data() {
return {
imageList: [
'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/191c1679754c4ad0ab8e32771968ff94~tplv-k3u1fbpfcp-watermark.image?',
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8ccef1776e0b4887bc8d47fefc4cd388~tplv-k3u1fbpfcp-watermark.image?',
'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/70dada16aa4845698f893a4d2bddcfb7~tplv-k3u1fbpfcp-watermark.image?'
],
showPreview: true
}
},
methods: {
close() {
this.showPreview = false;
}
}
}
</script>
拖动
图片拖动事件也比较好实现,咱们也不用做边界情况判断,反正拖动多少就是多少,比较简单我们直接来看代码:
<template>
<div ref="ydPreview" tabindex="-1" class="yd-preview">
...
<!-- 展示区 -->
<div class="preview__canvas">
<template v-for="(url, i) in urlList">
<img
v-if="i === index"
ref="img"
:key="url"
:src="currentImg"
:style="imgStyle"
@load="handleImgLoad"
@error="handleImgError"
@mousedown="handleMouseDown"
/>
</template>
...
</div>
...
</div>
</template>
<script>
export default {
props: {
...,
onClose: {
type: Function,
default: () => {}
}
},
methods: {
// 拖动
handleMouseDown (e) {
if (this.loading || e.button !== 0) return;
const { offsetX, offsetY } = this.transform;
const startX = e.pageX;
const startY = e.pageY;
this._dragHandler = rafThrottle((ev) => {
this.transform.offsetX = offsetX + ev.pageX - startX;
this.transform.offsetY = offsetY + ev.pageY - startY;
})
on(document, 'mousemove', this._dragHandler);
on(document, 'mouseup', (ev) => {
off(document, 'mousemove', this._dragHandler); // 解绑事件
})
e.preventDefault();
},
}
}
</script>
<style scoped>
...
.preview__canvas:active{
cursor: grab;
}
</style>
写到这里, 图片预览组件 的核心功能就基本讲完了,剩下的小功能,像入场动画、组件追加 Body
下渲染、点击蒙层是否关闭、组件层级调整等等,倔友们就自行看下方的完整源码悟囖。
完整源码
<template>
<transition name="viewer-fade">
<div ref="ydPreview" tabindex="-1" class="yd-preview" :style="{ 'z-index': zIndex }">
<div @click.self="handleMaskClick" class="preview__mask"></div>
<span class="preview__btn preview__close" @click="hideEvent">
<svg t="1669452473618" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4022" width="48" height="48"><path d="M571.733333 512l268.8-268.8c17.066667-17.066667 17.066667-42.666667 0-59.733333-17.066667-17.066667-42.666667-17.066667-59.733333 0L512 452.266667 243.2 183.466667c-17.066667-17.066667-42.666667-17.066667-59.733333 0-17.066667 17.066667-17.066667 42.666667 0 59.733333L452.266667 512 183.466667 780.8c-17.066667 17.066667-17.066667 42.666667 0 59.733333 8.533333 8.533333 19.2 12.8 29.866666 12.8s21.333333-4.266667 29.866667-12.8L512 571.733333l268.8 268.8c8.533333 8.533333 19.2 12.8 29.866667 12.8s21.333333-4.266667 29.866666-12.8c17.066667-17.066667 17.066667-42.666667 0-59.733333L571.733333 512z" p-id="4023" fill="#ffffff"></path></svg>
</span>
<div class="preview__canvas">
<template v-for="(url, i) in urlList">
<img
v-if="i === index"
ref="img"
:key="url"
:src="currentImg"
:style="imgStyle"
@load="handleImgLoad"
@error="handleImgError"
@mousedown="handleMouseDown"
/>
</template>
<svg v-if="loading" class="preview__loading" viewBox="25 25 50 50">
<circle class="preview__loading-path" cx="50" cy="50" r="20" fill="none"/>
</svg>
</div>
<div class="preview__btn preview__actions">
<div class="actions__inner">
<svg @click.stop="handleActions('zoomIn')" class="actions__icon" t="1669452513061" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4278" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4279" fill="#ffffff"></path><path d="M597.333333 437.333333h-96V341.333333c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v96H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h96V597.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-96H597.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4280" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('zoomOut')" class="actions__icon" t="1669452664869" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4529" width="48" height="48"><path d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" p-id="4530" fill="#ffffff"></path><path d="M597.333333 437.333333H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" p-id="4531" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('anticlocelise')" class="actions__icon" t="1669452861191" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8599" width="48" height="48"><path d="M511.4 124C290.5 124.3 112 303 112 523.9c0 128 60.2 242 153.8 315.2l-37.5 48c-4.1 5.3-0.3 13 6.3 12.9l167-0.8c5.2 0 9-4.9 7.7-9.9L369.8 727c-1.6-6.5-10-8.3-14.1-3L315 776.1c-10.2-8-20-16.7-29.3-26-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-7.5 7.5-15.3 14.5-23.4 21.2-3.4 2.8-3.9 7.7-1.2 11.1l39.4 50.5c2.8 3.5 7.9 4.1 11.4 1.3C854.5 760.8 912 649.1 912 523.9c0-221.1-179.4-400.2-400.6-399.9z" p-id="8600" fill="#ffffff"></path></svg>
<svg @click.stop="handleActions('clocelise')" class="actions__icon" t="1669452818298" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8349" width="48" height="48"><path d="M758.2 839.1C851.8 765.9 912 651.9 912 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1-8.1-6.6-15.9-13.7-23.4-21.2-29.4-29.4-52.5-63.6-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-9.3 9.3-19.1 18-29.3 26L668.2 724c-4.1-5.3-12.5-3.5-14.1 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 0.8c6.7 0 10.5-7.7 6.3-12.9l-37.3-47.9z" p-id="8350" fill="#ffffff"></path></svg>
</div>
</div>
<template v-if="!isSingle">
<span @click="prevEvent" class="preview__btn preview__prev" :class="{ 'is-disabled': !infinite && isFirst }">
<svg t="1669452168825" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3772" width="48" height="48"><path d="M384 512L731.733333 202.666667c17.066667-14.933333 19.2-42.666667 4.266667-59.733334-14.933333-17.066667-42.666667-19.2-59.733333-4.266666l-384 341.333333c-10.666667 8.533333-14.933333 19.2-14.933334 32s4.266667 23.466667 14.933334 32l384 341.333333c8.533333 6.4 19.2 10.666667 27.733333 10.666667 12.8 0 23.466667-4.266667 32-14.933333 14.933333-17.066667 14.933333-44.8-4.266667-59.733334L384 512z" p-id="3773" fill="#ffffff"></path></svg>
</span>
<span @click="nextEvent" class="preview__btn preview__next" :class="{ 'is-disabled': !infinite && isLast }">
<svg t="1669452135423" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3522" width="48" height="48"><path d="M731.733333 480l-384-341.333333c-17.066667-14.933333-44.8-14.933333-59.733333 4.266666-14.933333 17.066667-14.933333 44.8 4.266667 59.733334L640 512 292.266667 821.333333c-17.066667 14.933333-19.2 42.666667-4.266667 59.733334 8.533333 8.533333 19.2 14.933333 32 14.933333 10.666667 0 19.2-4.266667 27.733333-10.666667l384-341.333333c8.533333-8.533333 14.933333-19.2 14.933334-32s-4.266667-23.466667-14.933334-32z" p-id="3523" fill="#ffffff"></path></svg>
</span>
</template>
</div>
</transition>
</template>
<script>
import { on, off, isFirefox, rafThrottle } from './utils.js'
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
export default {
props: {
urlList: {
type: Array,
default: () => []
},
zIndex: {
type: Number,
default: 2000
},
onSwitch: {
type: Function,
default: () => {}
},
initialIndex: {
type: Number,
default: 0
},
onClose: {
type: Function,
default: () => {}
},
appendToBody: {
type: Boolean,
default: true
},
maskClosable: {
type: Boolean,
default: true
},
zIndex: {
type: Number,
default: 2000
},
},
data() {
return {
index: this.initialIndex,
transform: {
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false,
},
infinite: true,
loading: false,
}
},
computed: {
currentImg () {
return this.urlList[this.index]
},
imgStyle () {
const { scale, deg, offsetX, offsetY, enableTransition } = this.transform
const style = {
transform: `scale(${scale}) rotate(${deg}deg)`,
transition: enableTransition ? 'transform .3s' : '',
'margin-left': `${offsetX}px`,
'margin-top': `${offsetY}px`
}
style.maxWidth = style.maxHeight = '100%'
return style;
},
isFirst () {
return this.index === 0
},
isLast () {
return this.index === this.urlList.length - 1
},
isSingle () {
return this.urlList.length <= 1
}
},
watch: {
currentImg () {
this.$nextTick(() => {
const $img = this.$refs.img[0]
if (!$img.complete) {
this.loading = true
}
})
},
index: {
handler: function (val) {
this.reset()
this.onSwitch(val)
}
},
},
mounted () {
this.deviceSupportInstall()
if (this.appendToBody) {
document.body.appendChild(this.$el)
}
// 防止文档滚动带来的问题
this.$refs['ydPreview'].focus()
},
methods: {
reset () {
this.transform = {
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false
}
},
handleMaskClick() {
this.maskClosable && this.hideEvent()
},
handleMouseDown (e) {
if (this.loading || e.button !== 0) return
const { offsetX, offsetY } = this.transform
const startX = e.pageX
const startY = e.pageY
this._dragHandler = rafThrottle((ev) => {
this.transform.offsetX = offsetX + ev.pageX - startX
this.transform.offsetY = offsetY + ev.pageY - startY
})
on(document, 'mousemove', this._dragHandler)
on(document, 'mouseup', (ev) => {
off(document, 'mousemove', this._dragHandler)
})
e.preventDefault()
},
handleImgLoad (e) {
this.loading = false
},
handleImgError (e) {
this.loading = false
e.target.alt = '加载失败'
},
prevEvent () {
if (this.isFirst && !this.infinite) return
const len = this.urlList.length
this.index = (this.index - 1 + len) % len
},
nextEvent () {
if (this.isLast && !this.infinite) return
const len = this.urlList.length
this.index = (this.index + 1) % len
},
handleActions (action, options = {}) {
if (this.loading) return
const { zoomRate, rotateDeg, enableTransition } = {
zoomRate: 0.2,
rotateDeg: 90,
enableTransition: true,
...options
}
const { transform } = this
switch (action) {
case 'zoomOut':
if (transform.scale > 0.2) {
transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3))
}
break
case 'zoomIn':
transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3))
break
case 'clocelise':
transform.deg += rotateDeg
break
case 'anticlocelise':
transform.deg -= rotateDeg
break
}
transform.enableTransition = enableTransition
},
hideEvent () {
this.deviceSupportUninstall();
this.onClose()
},
deviceSupportInstall () {
this._keyDownHandler = rafThrottle((e) => {
const keyCode = e.keyCode
switch (keyCode) {
case 27:
this.hideEvent()
break
case 37:
this.prevEvent()
break
case 38:
this.handleActions('zoomIn')
break
case 39:
this.nextEvent()
break
case 40:
this.handleActions('zoomOut')
break
}
})
this._mouseWheelHandler = rafThrottle((e) => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail
if (delta > 0) {
this.handleActions('zoomIn', {
zoomRate: 0.015,
enableTransition: false
})
} else {
this.handleActions('zoomOut', {
zoomRate: 0.015,
enableTransition: false
})
}
})
on(document, 'keydown', this._keyDownHandler)
on(document, mousewheelEventName, this._mouseWheelHandler)
},
deviceSupportUninstall () {
off(document, 'keydown', this._keyDownHandler)
off(document, mousewheelEventName, this._mouseWheelHandler)
this._keyDownHandler = null
this._mouseWheelHandler = null
}
},
destroyed() {
if (this.appendToBody && this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
}
}
</script>
<style scoped>
.yd-preview {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.preview__mask {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0.5;
background: #000;
}
.preview__btn {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
opacity: 0.8;
cursor: pointer;
box-sizing: border-box;
user-select: none;
}
.preview__close {
top: 40px;
right: 40px;
width: 40px;
height: 40px;
font-size: 40px;
color: #fff;
}
.preview__canvas {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview__canvas:active{
cursor: grab;
}
.preview__actions {
left: 50%;
bottom: 30px;
transform: translateX(-50%);
width: 282px;
height: 44px;
padding: 0 23px;
background-color: #606266;
border-color: #fff;
border-radius: 22px;
}
.actions__inner {
width: 100%;
height: 100%;
text-align: justify;
cursor: default;
font-size: 23px;
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
align-items: center;
justify-content: space-around;
}
.actions__icon {
cursor: pointer;
width: 30px;
}
.preview__next,
.preview__prev {
top: 50%;
width: 44px;
height: 44px;
font-size: 24px;
color: #fff;
background-color: #606266;
border-color: #fff;
}
.preview__prev {
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
left: 40px;
}
.preview__next {
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
right: 40px;
text-indent: 2px;
}
.preview__prev svg{
width: 28px;
margin-right: 3px;
}
.preview__next svg {
width: 28px;
margin-left: 3px;
}
.preview__loading {
position: absolute;
height: 50px;
width: 50px;
animation: loading-rotate 2s linear infinite;
}
.preview__loading-path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2px;
stroke: rgba(255, 255, 255, 0.8);
stroke-linecap: round;
}
@keyframes loading-rotate {
100% {
transform: rotate(360deg)
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
.is-disabled {
cursor: no-drop !important;
}
.viewer-fade-enter-active {
-webkit-animation: viewer-fade-in 0.3s;
animation: viewer-fade-in 0.3s;
}
.viewer-fade-leave-active {
-webkit-animation: viewer-fade-out 0.3s;
animation: viewer-fade-out 0.3s;
}
@keyframes viewer-fade-in {
0% {
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
opacity: 0;
}
100% {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@keyframes viewer-fade-out {
0% {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
opacity: 1;
}
100% {
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
opacity: 0;
}
}
</style>
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。