Vue 3:基于按钮切换动态图片展示(附Demo)
目录
- 前言
- 1. Demo
- 2. 升级Demo
- 3. 终极Demo
前言
原先写过类似的知识点:
- 详细分析el-breadcrumb 面包屑的基本知识(附Demo)
- 详细分析el-card中的基本知识(附Demo)
本篇博客将介绍如何通过点击按钮切换不同的图片,并优化交互体验,使页面更流畅、响应更快
主要的知识点:
- Vue 3 组件化开发 - 通过 setup 组合式 API 进行数据管理
- 动态绑定 class - 根据当前选中的图片高亮按钮
- 过渡动画 transition - 让图片切换更流畅
- 图片预加载 - 避免切换时的卡顿
- 样式优化 - 让按钮和图片展示更美观
1. Demo
最初始的版本Demo:
<template>
<div>
<!-- 按钮组 -->
<div class="button-group">
<button v-for="image in images" :key="image.id" @click="currentImage = image.src">
{{ image.label }}
</button>
</div>
<!-- 动态展示图片 -->
<div class="image-container">
<img v-if="currentImage" :src="currentImage" alt="Dynamic Image" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 当前选中的图片
const currentImage = ref(null)
// 定义按钮与对应的图片路径(public 目录下的图片)
const images = [
{ id: 'img1', label: '图片1', src: '/image1.png' },
{ id: 'img2', label: '图片2', src: '/image2.png' },
{ id: 'img3', label: '图片3', src: '/image3.png' },
{ id: 'img4', label: '图片4', src: '/image4.png' }
]
</script>
<style scoped>
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 12px;
cursor: pointer;
}
.image-container {
text-align: center;
}
img {
max-width: 100%;
height: auto;
}
</style>
截图如下:
2. 升级Demo
这一版本有个缺点,就是卡顿,但是他的样式很好看!
- 图片加载问题:每次点击都会重新加载图片,导致延迟
- 样式导致的渲染卡顿:box-shadow、border 变化可能导致重绘
- 事件未优化:Vue 响应式 ref 更新时,可能导致不必要的 DOM 计算
更改相关样式以及按钮:
<template>
<div class="container">
<!-- 按钮组 -->
<div class="button-group">
<button v-for="image in images" :key="image.id" @click="currentImage = image.src" class="image-button">
<img :src="image.src" alt="Preview" class="button-thumbnail" />
<span>{{ image.label }}</span>
</button>
</div>
<!-- 动态展示图片 -->
<div class="image-container">
<img v-if="currentImage" :src="currentImage" alt="Dynamic Image" class="main-image" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 当前选中的图片
const currentImage = ref(null)
// 定义按钮与对应的图片路径(public 目录下的图片)
const images = [
{ id: 'img1', label: '图片1', src: '/favicon.ico' },
{ id: 'img2', label: '图片2', src: '/image2.png' },
{ id: 'img3', label: '图片3', src: '/image3.png' },
{ id: 'img4', label: '图片4', src: '/image4.png' },
{ id: 'img5', label: '图片5', src: '/deepSeaField.png' },
{ id: 'img6', label: '图片6', src: '/image2.png' },
{ id: 'img7', label: '图片7', src: '/image3.png' },
{ id: 'img8', label: '图片8', src: '/image4.png' }
]
</script>
<style scoped>
/* 整体容器 */
.container {
max-width: 800px;
margin: 0 auto;
text-align: center;
}
/* 按钮组:使用网格布局 */
.button-group {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4 列布局 */
gap: 15px;
margin-bottom: 20px;
}
/* 按钮样式 */
.image-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
border: 2px solid #ddd;
border-radius: 12px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease-in-out;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
}
.image-button:hover {
background: #f0f0f0;
transform: scale(1.05);
}
/* 按钮里的小缩略图 */
.button-thumbnail {
width: 50px;
height: 50px;
border-radius: 8px;
object-fit: cover;
margin-bottom: 5px;
}
/* 主要图片 */
.image-container {
margin-top: 20px;
}
.main-image {
max-width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
</style>
截图如下:
后续继续整改优化:
<template>
<div class="container">
<!-- 按钮组 -->
<div class="button-group">
<button v-for="image in images" :key="image.id" @click="currentImage = image.src" class="image-button">
<img :src="image.src" alt="Preview" class="button-thumbnail" />
<span>{{ image.label }}</span>
</button>
</div>
<!-- 动态展示图片 -->
<div class="image-container">
<img v-if="currentImage" :src="currentImage" alt="Dynamic Image" class="main-image" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 当前选中的图片
const currentImage = ref(null)
// 定义按钮与对应的图片路径(public 目录下的图片)
const images = [
{ id: 'img1', label: '图片1', src: '/favicon.ico' },
{ id: 'img2', label: '图片2', src: '/image2.png' },
{ id: 'img3', label: '图片3', src: '/image3.png' },
{ id: 'img4', label: '图片4', src: '/image4.png' },
{ id: 'img5', label: '图片5', src: '/deepSeaField.png' },
{ id: 'img6', label: '图片6', src: '/image2.png' },
{ id: 'img7', label: '图片7', src: '/image3.png' },
{ id: 'img8', label: '图片8', src: '/image4.png' }
]
</script>
<style scoped>
/* 整体容器 */
.container {
max-width: 1000px;
margin: 0 auto;
text-align: center;
}
/* 按钮组:一行排列 */
.button-group {
display: flex;
justify-content: space-around;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap; /* 保证小屏设备时自动换行 */
}
/* 按钮样式 */
.image-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
border: 2px solid #ddd;
border-radius: 12px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease-in-out;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
width: 100px; /* 固定宽度 */
height: 120px; /* 固定高度 */
}
.image-button:hover {
background: #f0f0f0;
transform: scale(1.05);
}
/* 按钮里的小缩略图 */
.button-thumbnail {
width: 50px;
height: 50px;
border-radius: 8px;
object-fit: cover;
margin-bottom: 5px;
}
/* 主要图片 */
.image-container {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
height: 600px; /* 固定高度,居中展示大图 */
border: 2px solid #ddd;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.main-image {
max-width: 100%;
max-height: 100%; /* 确保图片不会超出容器 */
object-fit: contain; /* 保持图片比例 */
}
</style>
截图如下:
3. 终极Demo
后续发现会很卡顿
优化点
-
去除黑色边框:
由于<button>
在不同浏览器默认会有 outline 选中效果,添加 outline: none 解决
通过 border: none 避免额外边框影响 -
流畅过渡动画:
按钮缩略图 采用 background-image,提高图片加载速度
主图片切换 采用 Vue Transition 并优化 opacity 过渡,使切换更顺畅 -
减少卡顿:
预加载所有图片,防止首次切换时加载延迟
优化 changeImage 逻辑,避免重复更新 currentImage
<template>
<div class="container">
<!-- 按钮组 -->
<div class="button-group">
<button
v-for="image in images"
:key="image.id"
@click="changeImage(image.src)"
class="image-button"
:class="{ active: currentImage === image.src }"
>
<div class="button-thumbnail" :style="{ backgroundImage: `url(${image.src})` }"></div>
<span>{{ image.label }}</span>
</button>
</div>
<!-- 动态展示图片 -->
<div class="image-container">
<transition name="fade" mode="out-in">
<img v-if="currentImage" :src="currentImage" alt="Dynamic Image" class="main-image" />
</transition>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 定义按钮与对应的图片路径
const images = [
{ id: 'img1', label: '未来制造', src: '/futureManufacturing.png' },
{ id: 'img2', label: '未来材料', src: '/futureMaterials.png' },
{ id: 'img3', label: '未来信息', src: '/futureInformation.png' },
{ id: 'img4', label: '未来能源', src: '/futureEnergy.png' },
{ id: 'img5', label: '未来医疗', src: '/futureMedical.png' },
{ id: 'img6', label: '人工智能', src: '/artificialIntelligence.png' },
{ id: 'img7', label: '数字经济', src: '/digitalEconomy.png' },
{ id: 'img8', label: '低空经济', src: '/lowAltitudeEconomy.png' },
{ id: 'img9', label: '深海领域', src: '/deepSeaField.png' }
]
// 默认选中第一张图片
const currentImage = ref(images[0].src)
// 切换图片
const changeImage = (src) => {
if (currentImage.value !== src) {
currentImage.value = src
}
}
// 预加载图片,减少切换时的卡顿
const preloadImages = () => {
images.forEach(image => {
const img = new Image()
img.src = image.src
})
}
onMounted(preloadImages)
</script>
<style scoped>
/* 整体容器 */
.container {
max-width: 1200px;
margin: 0 auto;
text-align: center;
}
/* 按钮组 */
.button-group {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: nowrap;
overflow-x: auto;
padding: 10px 0;
scrollbar-width: none; /* 隐藏滚动条 */
}
/* 按钮样式 */
.image-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
border: none;
border-radius: 10px;
padding: 5px;
cursor: pointer;
transition: transform 0.2s ease, background 0.2s ease;
width: 90px;
height: 110px;
outline: none; /* 去除点击时的黑框 */
}
.image-button:hover {
transform: scale(1.05);
}
/* 选中状态 */
.image-button.active {
background: #e3f2fd;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
/* 按钮里的小缩略图 */
.button-thumbnail {
width: 50px;
height: 50px;
border-radius: 8px;
background-size: cover;
background-position: center;
margin-bottom: 5px;
}
/* 主要图片 */
.image-container {
will-change: transform, opacity;
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
height: 600px;
border-radius: 10px;
width: 100%; /* 增加宽度 */
max-width: 1200px; /* 限制最大宽度,防止过大 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.main-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* 过渡动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease-in-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>