参考于(参考方案支持pc端与移动端):原生 JS 手写一个优雅的图片预览功能,带你吃透背后原理 - 知乎 (zhihu.com)
可当作一个vue组件通过ref调取初始化监听事件addlistionAll函数调用()传递的数据为图片路径,如果要预览多张图片可自行修改。
以下为vue文件代码:
<template>
<div id="list" class="modal" v-if="state.visable">
<img class="item" :src="state.imageUrls" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
// import { require } from '@/utlis/cont'
// import { useRouter } from 'vue-router'
import { useMain } from "@/store/home";
import { storeToRefs } from "pinia";
export default defineComponent({
setup(_props) {
const state = reactive({
visable: false,
imageUrls: "" as any,
showPreview: false,
startPoint: { x: 0, y: 0 }, // 记录初始触摸点位
isTouching: false, // 标记是否正在移动
isMove: false, // 正在移动中,与点击做区别
offset: {
left: 0,
top: 0,
} as any, //鼠标/手指移动
imgdome: "" as any, //当前拖动的dom元素
scale: 1, // 放大或缩小
lastScale: 1, // 最后的缩放值
origin: "center", //点击拖动的位置
touches: new Map(), // 记录触摸点数组 根据数组长度来判断是单击、双击
lastDistance: 0,
initialData: { offset: { left: 0, top: 0 }, origin: "center", scale: 1 },
timer: "" as any, // 还原记录,用于边界处理
scaleOrigin: { x: 0, y: 0 } as any,
});
onMounted(() => {});
// const router = useRouter()
const store = useMain();
const states = storeToRefs(store);
// 函数式初始化调用
const openPreview = (src: any) => {
state.scale = 1;
state.visable = true;
state.imageUrls = src;
state.imgdome = "";
const settime = setInterval(() => {
if (state.imgdome) {
clearInterval(settime);
return;
}
addlistionAll();
}, 500);
};
// 初始化监听事件
const addlistionAll = () => {
state.imgdome = document.querySelector(".item");
if (!state.imgdome) return;
console.log(state.imgdome);
document
.querySelector("#list")!
.addEventListener("mousewheel", zoom, { passive: false });
//根据数组长度来判断是单击、双击
window.addEventListener("pointerdown", function (e) {
e.preventDefault();
state.touches.set(e.pointerId, e); // TODO: 点击存入触摸点
state.isTouching = true;
state.startPoint = { x: e.clientX, y: e.clientY };
if (state.touches.size === 2) {
// TODO: 判断双指触摸,并立即记录初始数据
state.lastDistance = getDistance();
state.lastScale = state.scale;
}
});
// 鼠标/手指按下
window.addEventListener("pointerdown", function (e) {
e.preventDefault();
state.touches.set(e.pointerId, e); // TODO: 点击存入触摸点
state.isTouching = true;
state.startPoint = { x: e.clientX, y: e.clientY };
if (state.touches.size === 2) {
// TODO: 判断双指触摸,并立即记录初始数据
state.lastDistance = getDistance();
state.lastScale = state.scale;
}
});
// 鼠标/手指抬起
window.addEventListener("pointerup", function (e) {
state.touches.delete(e.pointerId); // TODO: 抬起移除触摸点
if (state.touches.size <= 0) {
state.isTouching = false;
} else {
const touchArr = Array.from(state.touches);
// 更新点位
state.startPoint = {
x: touchArr[0][1].clientX,
y: touchArr[0][1].clientY,
};
}
setTimeout(() => {
state.isMove = false;
}, 300);
});
// 鼠标/手指移动
window.addEventListener("pointermove", (e) => {
if (state.isTouching) {
state.isMove = true;
// console.log(state.touches.size,state.scale);
if (state.touches.size < 2) {
console.log(
state.offset.left % 1 == 0 || state.offset.top % 1 == 0
);
// if (!(state.offset.left % 1 == 0 || state.offset.top % 1 == 0))
// return;
// 单指滑动/鼠标移动
state.offset = {
left: +state.offset.left + (e.clientX - state.startPoint.x),
top: +state.offset.top + (e.clientY - state.startPoint.y),
};
changeStyle(state.imgdome, [
"transition: all 0s",
`transform: translate(${state.offset.left + "px"}, ${
state.offset.top + "px"
}) scale(${state.scale}) `,
`transform-origin: ${state.origin}`,
]);
// 注意移动完也要更新初始点位,否则图片会加速逃逸可视区域
state.startPoint = { x: e.clientX, y: e.clientY };
} else {
// 双指缩放
state.touches.set(e.pointerId, e);
const ratio = getDistance() / state.lastDistance;
state.scale = ratio * state.lastScale;
state.offset = getOffsetCorrection();
if (state.scale < state.initialData.scale) {
reduction();
}
changeStyle(state.imgdome, [
"transition: all 0s",
`transform: translate(${state.offset.left + "px"}, ${
state.offset.top + "px"
}) scale(${state.scale})`,
`transform-origin: ${state.origin}`,
]);
}
}
});
window.addEventListener("pointercancel", function (_e) {
state.touches.clear(); // 可能存在特定事件导致中断,所以需要清空
});
// 将当前url地址添加到浏览器的历史记录中
window.history.pushState(null, "", document.URL); //第三个参数不写默认为当前url地址
// 监听物理返回
window.addEventListener(
"popstate",
function () {
state.visable = false;
window.removeEventListener("popstate", function () {}, false);
},
false
);
};
// 用于修改样式的工具类,并且可以减少回流重绘,后面代码中会频繁用到
const changeStyle = (el: any, arr: any) => {
const original = el.style.cssText.split(";");
original.pop();
el.style.cssText = original.concat(arr).join(";") + ";";
};
// 获取中心改变的偏差
const getOffsetCorrection = (x = 0, y = 0) => {
const touchArr = Array.from(state.touches);
if (touchArr.length === 2) {
const start = touchArr[0][1];
const end = touchArr[1][1];
x = (start.offsetX + end.offsetX) / 2;
y = (start.offsetY + end.offsetY) / 2;
}
state.origin = `${x}px ${y}px`;
const offsetLeft =
(state.scale - 1) * (x - state.scaleOrigin.x) + state.offset.left;
const offsetTop =
(state.scale - 1) * (y - state.scaleOrigin.y) + state.offset.top;
state.scaleOrigin = { x, y };
return { left: offsetLeft, top: offsetTop };
};
// 获取两个手指之间的距离
const getDistance = () => {
const touchArr = Array.from(state.touches);
if (touchArr.length < 2) {
return 0;
}
const start = touchArr[0][1];
const end = touchArr[1][1];
return Math.hypot(end.x - start.x, end.y - start.y);
};
// // 记录初始化数据
// const record = () => {
// state.initialData = Object.assign(
// {},
// { offset: state.offset, origin: state.origin, scale: state.scale }
// );
// };
// 缩放
const zoom = (event: any) => {
console.log(state.origin);
if (!event.deltaY) {
return;
}
event.preventDefault();
state.origin = `${event.offsetX}px ${event.offsetY}px`;
// 缩放执行
if (event.deltaY < 0) {
state.scale += 0.1; // 放大
} else if (event.deltaY > 0) {
state.scale >= 0.2 && (state.scale -= 0.1); // 缩小
}
if (state.scale < state.initialData.scale) {
reduction();
}
state.offset = getOffsetCorrection(event.offsetX, event.offsetY);
changeStyle(state.imgdome, [
"transition: all .15s",
`transform-origin: ${state.origin}`,
`transform: translate(${state.offset.left + "px"}, ${
state.offset.top + "px"
}) scale(${state.scale})`,
]);
};
// 还原记录,用于边界处理
const reduction = () => {
state.timer && clearTimeout(state.timer);
state.timer = setTimeout(() => {
state.offset = state.initialData.offset;
state.origin = state.initialData.origin;
state.scale = state.initialData.scale;
changeStyle(state.imgdome, [
`transform: translate(${state.offset.left + "px"}, ${
state.offset.top + "px"
}) scale(${state.scale})`,
`transform-origin: ${state.origin}`,
]);
}, 100);
};
return {
state,
states,
openPreview,
};
},
});
</script>
<style lang="scss" scoped>
/* 图片预览 */
.modal {
touch-action: none;
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #222;
user-select: none;
img {
position: absolute;
padding: 0;
margin: 0;
object-fit: contain;
width: 100vw;
height: 100vh;
/* transition: all var(--delay_time); */
transform: translateZ(0);
}
}
</style>