基于vue3实现的图片预览功能,小图列表展示所有图片,切换当前预览列表滚动到可视区,滚轮滑动查看列表图片,键盘切换当前预览; 相关技术:vue3, element-plus, @element-plus/icons-vue, vue-lazyload 效果如下:
代码如下:
<script lang="ts" setup>
import { ref, onMounted } from "vue"
import defaultThumbnail from "@/assets/images/gray64.png"
const currentPreivewPic = ref(0)
const imageErrorList = ref([
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR_ENy58lE94vL4sKJxi22863djqbz4nMrf1Q&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQmwtS8uahm2WQs8CnEXE3ZpQZtsM2HFeuvQg&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTPSMsOf0WN_PQttn2O-8kMif2L4pv7aGkcZg&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzGm9bLZ11p8cKDbHBopJcsMbV0Gx6-mvhSQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzGm9bLZ11p8cKDbHBopJcsMbV0Gx6-mvhSQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQI8j8H1yJZUx7Y2p9d-nj8o7A2i29bj6pjbA&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR_ENy58lE94vL4sKJxi22863djqbz4nMrf1Q&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQmwtS8uahm2WQs8CnEXE3ZpQZtsM2HFeuvQg&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-2VpmZhsrAgOHxVl1vDzWMZzXR3Sqt06tLQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTPSMsOf0WN_PQttn2O-8kMif2L4pv7aGkcZg&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzGm9bLZ11p8cKDbHBopJcsMbV0Gx6-mvhSQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzGm9bLZ11p8cKDbHBopJcsMbV0Gx6-mvhSQ&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQI8j8H1yJZUx7Y2p9d-nj8o7A2i29bj6pjbA&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR9eZkD6dfj6ISTOnby9qpn_YGSqimpFW0l4A&usqp=CAU"
])
const scrollWrapRef = ref<any>(null)
const scrollListRef = ref<any>(null)
const preIndexRef = ref<any>(null)
const nexIndexRef = ref<any>(null)
const itemWidth = ref(222) // 每张图片的固定宽度
onMounted(() => {
// 监听键盘事件
document.addEventListener("keydown", handleKeyDown)
handleElementToView(currentPreivewPic.value, true)
return () => {
document.removeEventListener("keydown", handleKeyDown)
}
})
/*处理鼠标滚动事件*/
const handleScroll = (e: any) => {
if (e.currentTarget.className === "img-list-wrap") {
const maxScrollLeft = scrollListRef.value.offsetWidth - scrollWrapRef.value.offsetWidth
const delta = e.deltaY
const scrollDistance =
delta < 0 ? scrollWrapRef.value.scrollLeft - itemWidth.value : scrollWrapRef.value.scrollLeft + itemWidth.value
const targetDistance = scrollDistance > maxScrollLeft ? maxScrollLeft : scrollDistance < 0 ? 0 : scrollDistance
scrollWrapRef.value.scrollLeft = targetDistance
}
return false
}
/*键盘事件*/
const handleKeyDown = (e: any) => {
//键盘按键判断: 左箭头-37; 上箭头-38;右箭头-39;下箭头-40 PageUp-33; PageDown-34; Esc-27;
const keyCode = e.keyCode
if (keyCode === 37 || keyCode === 38 || keyCode === 33) {
// 切换上一页
handlePreIndex()
} else if (keyCode === 39 || keyCode === 40 || keyCode === 34) {
// 切换下一页
handleNextIndex()
} else if (keyCode === 27) {
// Esc退出
}
}
/*处理上一页逻辑*/
const handlePreIndex = () => {
if (currentPreivewPic.value > 0) {
const index = currentPreivewPic.value - 1
currentPreivewPic.value = index
handleElementToView(index)
}
}
/*处理上一页逻辑*/
const handleNextIndex = () => {
if (currentPreivewPic.value < imageErrorList.value.length - 1) {
const index = currentPreivewPic.value + 1
currentPreivewPic.value = index
handleElementToView(index)
}
}
/*更新当前预览图片*/
const changePreview = (index: number) => {
if (index === currentPreivewPic.value) return
currentPreivewPic.value = index
handleElementToView(index)
}
/*判断元素是否在可视区内, 并让元素滚动到可视区*/
const handleElementToView = (index: number, isFirstRender = false) => {
// 默认当前视图滚动到正中间
setTimeout(() => {
const wrapWidth = scrollWrapRef.value.offsetWidth
const wrapScrollLeft = scrollWrapRef.value.scrollLeft
// 只有 index >= 3 && index <= (store.imageErrorList.length - 3) 才可以移动到最中间的位置
const centerNumber = Math.floor((wrapWidth / itemWidth.value) / 2)
let centerIndex = index
if (index >= centerNumber && index <= (imgCloudStore.imageErrorList.length - centerNumber)) centerIndex = index + centerNumber
if (((wrapScrollLeft + wrapWidth - itemWidth.value) <= (itemWidth.value * centerIndex)) ||
(wrapScrollLeft * (itemWidth.value * centerNumber) > (itemWidth.value * centerIndex))
) { // 图片需要移动
const leftDis = itemWidth.value * (centerIndex + 1) - wrapWidth
scrollWrapRef.value.scrollTo({
left: leftDis >= 0 ? leftDis : 0,
behavior: !isFirstRender ? "smooth" : 'auto'
})
}
}, 0)
// 默认只滚动到可视区,不居中展示
// setTimeout(() => {
// const wrapWidth = scrollWrapRef.value.offsetWidth
// const wrapScrollLeft = scrollWrapRef.value.scrollLeft
// if (wrapScrollLeft > itemWidth.value * index) {
// // 图片隐藏在左边
// scrollWrapRef.value.scrollTo({
// left: itemWidth.value * index,
// behavior: !isFirstRender ? "smooth" : "auto"
// })
// } else if (wrapScrollLeft + wrapWidth - itemWidth.value < itemWidth.value * index) {
// // 图片隐藏在右边
// scrollWrapRef.value.scrollTo({
// left: itemWidth.value * (index + 1) - wrapWidth,
// behavior: !isFirstRender ? "smooth" : "auto"
// })
// }
// }, 0)
}
/*处理图片错误*/
const handleImgError = (e: any) => {
e.target.src = null
e.target.src = defaultThumbnail
}
</script>
<template>
<div class="image-preview-wrap">
<div class="preview-main-content">
<div class="preview-header">
<div class="title">图片预览</div>
<div class="operator-btn">
<el-icon
:style="{
fontSize: '25px',
color: 'eaeaea',
width: '50px',
height: '50px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer'
}"
><Close
/></el-icon>
</div>
</div>
<div class="preview-content">
<div class="img-main">
<!-- 预览大图 -->
<div class="large-wrap">
<!-- <div v-if="isFetching" class="loading-mask-large" v-loading="isFetching" /> -->
<div class="page-info">{{ currentPreivewPic + 1 + " / " + imageErrorList.length }}</div>
<el-icon ref="preIndexRef" @click="handlePreIndex" class="pre-index-btn index-btn"
><ArrowLeftBold
/></el-icon>
<el-icon ref="nexIndexRef" @click="handleNextIndex" class="next-index-btn index-btn"
><ArrowRightBold
/></el-icon>
<img :src="imageErrorList[currentPreivewPic]" @error="(e: any) => handleImgError(e)" alt="" />
</div>
<!-- 小图列表 -->
<div class="img-list-wrap" ref="scrollWrapRef" @wheel="(e: any) => handleScroll(e)">
<!-- <div v-if="isFetching" class="loading-mask-large" v-loading="isFetching" /> -->
<ul
class="img-list"
ref="scrollListRef"
draggable="false"
:style="{ width: itemWidth * imageErrorList.length + 'px' }"
>
<li
v-for="(item, index) in imageErrorList"
:key="index"
@click="changePreview(index)"
:class="['img-item', index === currentPreivewPic ? 'active-item ' : '']"
>
<img v-lazy="item" src="@/assets/images/gray64.png" alt="" />
</li>
</ul>
</div>
</div>
<div class="img-detail">
<div class="detail-title">基本信息, 展示当前预览的详细信息</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
// 图片预览组件单独封装
.image-preview-wrap {
position: fixed;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.8);
z-index: 2000;
padding: 10px;
.preview-main-content {
width: 100%;
height: 100%;
border-radius: 4px;
box-shadow: 1px 2px 1px #eaeaea;
overflow: hidden;
background-color: #ffffff;
.preview-header {
width: 100%;
height: 70px;
padding: 10px 15px 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 1px 1px 1px #eaeaea;
.title {
font-size: 20px;
}
.operator-btn {
display: flex;
justify-content: flex-end;
align-items: center;
}
}
.preview-content {
height: calc(100% - 70px);
width: 100%;
display: flex;
justify-content: space-between;
padding: 2px 10px 20px 10px;
.img-main {
width: 75%;
height: 100%;
.large-wrap {
width: 100%;
height: calc(100% - 150px);
background-color: rgb(0, 0, 0, 0.8);
position: relative;
user-select: none;
.page-info {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
color: #ffffff;
font-size: 20px;
width: 150px;
height: 40px;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.8);
}
.index-btn {
position: absolute;
z-index: 10;
color: #eaeaea;
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
cursor: pointer;
background: rgba(0, 0, 0, 0.2);
svg {
font-size: 30px;
}
&.pre-index-btn {
top: 50%;
left: 10px;
transform: translateY(-50%);
}
&.next-index-btn {
top: 50%;
right: 10px;
transform: translateY(-50%);
}
}
img {
width: 100%;
height: 100%;
object-fit: contain;
opacity: 1;
}
}
.img-list-wrap {
width: 100%;
height: 130px;
overflow-x: auto;
white-space: nowrap;
overflow-y: hidden;
position: relative;
margin-top: 15px;
&::-webkit-scrollbar {
display: none;
}
.img-list {
height: 130px;
padding: 0;
margin: 0;
cursor: pointer;
.img-item {
width: 222px;
height: 130px;
display: inline-block;
line-height: 130px;
padding: 0px;
cursor: pointer;
&.active-item {
margin-top: -5px;
img {
box-sizing: border-box;
opacity: 1;
width: 222px;
height: 130px;
padding: 0px;
border: 3px solid #ffc353;
transform-origin: center;
transform: scale(0.98);
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
}
}
img {
display: block;
width: 222px;
height: 130px;
padding: 2px;
opacity: 0.8;
&:hover {
box-sizing: border-box;
opacity: 1;
width: 222px;
height: 130px;
padding: 0px;
border: 3px solid #fc8746;
transform-origin: center;
transform: scale(0.98);
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
}
}
}
}
}
.loading-mask-large {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
.img-detail {
width: 25%;
height: 100%;
padding: 10px 20px;
.detail-title {
font-size: 20px;
}
.detail-item {
width: 100%;
font-size: 18px;
margin: 10px 0px;
span {
margin-right: 5px;
}
}
}
}
}
}
</style>
在main中需要引入vue-lazyload
import VueLazyload from "vue-lazyload"
app.use(VueLazyload, {
loading: defaultThumbnail,
error: defaultThumbnail,
dispatchEvent: true,
attempt: 1
})
app.use(VueLazyload)