像这种图片滑动对比的效果,网上还不少见吧,但是网上却不好找到完整现成的实现代码,我找到几个地方有类似的代码,但是都不好直接移植到代码里,因为很多都是使用原生html+css+js实现,太复杂了。反而不好应用到vue3中。
于是我借着他们的思路,自己实现了个。
前置条件
- 限制两张图片
- 图片大小必须一致,不一致会导致上层图片显示不全
- 底层图片必须存在,因为窗口的大小由他的大小决定
实现思路
- 将两张图片都看做是背景图,他们属于不同的层次
- 底层图片用相对定位,也就可以和其他元素一同正常展示,之后的所有元素都处于其包裹范围内,并且根据图片的大小确定整个样式的大小
- 上层图片用绝对定位,因为处于底层图片的包裹中,且大小是一样的,所以他们是完全重叠的,它的高度是和底层图片一致,但是宽度是可变的
- 利用input 的滑块模式
<input type="range" v-model="width" />
来改变上层图片的宽度,这里也就是利用了vue3的响应式。这是比原生方式实现的简便之处 - 由于
input
的原生样式无法改变,所以得额外做个滑块来实现自定义样式,然后其位置也受width
的控制,实际上并不是点击该滑块来滑动的 input
是出于最上层的,但是将其隐藏了,所以点击的时候看起来像是点击那个滑块,实际上点击的是input实现的滑动。- 然后再修饰下其他细节,即可
接下来逐步分解出实现的效果,这样就更好理解文字的意思了
第一步,将两张图片分别呈现
ts
<template>
<div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + bottomImg + ')' }">
<div class="upperImg" :style="{ backgroundImage: 'url(' + upperImg + ')', width: 100 - upperImgWidth + '%' }"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
const imgHeigth = ref("0px"); // 图片高度
const imgWidth = ref("0px"); // 图片宽度
const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图
const upperImgWidth = ref(50); // 上层图宽度
// 首次加载时初始化
onMounted(() => {
getImgSize();
});
// 获取图片尺寸
function getImgSize() {
//加载图片获取图片真实宽度和高度
var image = new Image();
image.onload = function () {
imgHeigth.value = image.height + "px";
imgWidth.value = image.width + "px";
};
image.src = bottomImg.value; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
}
</script>
<style>
.bottomImg {
position: relative; /* 相对定位 */
overflow: hidden; /* 隐藏超出部分 */
}
.upperImg {
position: absolute; /* 绝对定位 */
top: 0;
right: 0; /* 从右边开始铺开图片 */
height: 100%;
z-index: 1;
background-position: right top; /* 改变定位方式,默认是左上角,要改为右上角,这样才能实现底图在左边,上层图在右边的效果*/
border-left: 2px solid rgb(255, 255, 255, 0.5); /* 显示左边框 */
background-repeat: no-repeat; /* 不重复 */
}
</style>
代码有详细注释,现在两张图片分别展示的效果有了,但是还没法滑动。
第二步,添加滑块,图片动起来
就是加了个滑块,双向绑定宽度:
html
<!--滑块控制上层图片的宽度 -->
<input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />
样式
css
.inputRange {
position: absolute;
height: 100%; /* 这样就能点击任何位置都能实现移动滑块,而不仅仅是滑块所在的位置才有效*/
z-index: 3; /* 处于最高层次 */
left: -4px; /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
touch-action: auto;
width: calc(100% + 4px); /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
/* opacity: 0; */ /*隐藏滑块,这里为了演示,就不隐藏先*/
}
到这一步,效果已经有了,接下来就是样式的优化。
第三步,增加自定义的滑块
增加一个这样的滑块样式,其实他仅仅是个样式,它并不具备点击滑动的能力,他的位置是靠响应width
的值来改变的
html
<!-- 这是对外展示的滑块样式,仅仅是展示样式的,不然原生的样式不好看 -->
<span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>
下面的样式不是重点,自己调整都是可以的,只需要注意使用绝对定位,z-index
小于input
即可。
css
.spanHandle {
position: absolute; /*绝对定位还是一样的*/
z-index: 2; /* 样式很多,都不是关键的,只有这里,需要注意层次要低于inputRange*/
height: 48px;
width: 48px;
position: center;
font-size: 24px;
border: 1px;
border-radius: 50%;
top: calc(90% - 24px);
background-color: rgb(255, 255, 255, 0.5);
}
.spanHandle:before {
left: 5px;
transform: rotate(-45deg);
}
.spanHandle:after {
right: -5px;
transform: rotate(135deg);
}
.spanHandle:after,
.spanHandle:before {
border-left: 2px solid;
border-top: 2px solid;
content: "";
height: 10px;
position: absolute;
top: 50%;
transform-origin: 0 0;
width: 10px;
}
然后把上一步的opacity: 0;
启用即可隐藏原始的input滑块
第四步,添加label和上层图片为空时的效果,这一步是可选的
效果就不贴了,就跟开头时差不多
完整代码
<template>
<div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + bottomImg + ')' }">
<span class="imgLabel">{{ bottomLabel }}</span>
<div v-if="upperImg" class="upperImg" :style="{ backgroundImage: 'url(' + upperImg + ')', width: 100 - upperImgWidth + '%' }">
<span class="imgLabel">{{ upperLabel }}</span>
</div>
<div v-else class="upperUndefined" :style="{ width: 100 - upperImgWidth + '%' }">
<span class="undefinedSpan">暂无结果</span>
</div>
<!-- 这是对外展示的滑块样式,仅仅是展示样式的,不然原生的样式不好看 -->
<span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>
<!--滑块控制上层图片的宽度 -->
<input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
const imgHeigth = ref("0px"); // 图片高度
const imgWidth = ref("0px"); // 图片宽度
const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图
const upperImgWidth = ref(50); // 上层图宽度
const bottomLabel = ref("底图"); // 底图标签
const upperLabel = ref("上层图"); // 上层图标签
// 首次加载时初始化
onMounted(() => {
getImgSize();
});
// 获取图片尺寸
function getImgSize() {
//加载图片获取图片真实宽度和高度
var image = new Image();
image.onload = function () {
imgHeigth.value = image.height + "px";
imgWidth.value = image.width + "px";
};
image.src = bottomImg.value; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
}
</script>
<style>
.bottomImg {
position: relative; /* 相对定位 */
overflow: hidden; /* 隐藏超出部分 */
}
.upperImg {
position: absolute; /* 绝对定位 */
top: 0;
right: 0; /* 从右边开始铺开图片 */
height: 100%;
z-index: 1;
background-position: right top; /* 改变定位方式,默认是左上角,要改为右上角,这样才能实现底图在左边,上层图在右边的效果*/
border-left: 2px solid rgb(255, 255, 255, 0.5); /* 显示左边框 */
background-repeat: no-repeat; /* 不重复 */
}
.inputRange {
position: absolute;
height: 100%; /* 这样就能点击任何位置都能实现移动滑块,而不仅仅是滑块所在的位置才有效*/
z-index: 3; /* 处于最高层次 */
left: -4px; /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
touch-action: auto;
width: calc(100% + 4px); /*因为原始样式有边界,为了效果更好而调整的,这些都是实测效果,不重要*/
opacity: 0; /*隐藏滑块,这里为了演示,就不隐藏先*/
}
.spanHandle {
position: absolute; /*决定定位还是一样的*/
z-index: 2; /* 样式很多,都不是关键的,只有这里,需要注意层次要低于inputRange*/
height: 48px;
width: 48px;
position: center;
font-size: 24px;
border: 1px;
border-radius: 50%;
top: calc(90% - 24px);
background-color: rgb(255, 255, 255, 0.5);
}
.spanHandle:before {
left: 5px;
transform: rotate(-45deg);
}
.spanHandle:after {
right: -5px;
transform: rotate(135deg);
}
.spanHandle:after,
.spanHandle:before {
border-left: 2px solid;
border-top: 2px solid;
content: "";
height: 10px;
position: absolute;
top: 50%;
transform-origin: 0 0;
width: 10px;
}
.imgLabel {
font-size: 20px;
color: aliceblue;
text-shadow: 1px 1px #533d4a, 2px 2px #533d4a;
}
.upperUndefined {
position: absolute;
top: 0;
right: 0;
height: 100%;
z-index: 1;
font-size: 60px;
background-color: rgb(255, 255, 255, 0.8);
background-position: right top;
border-left: 2px solid rgb(255, 255, 255, 0.5);
}
</style>
封装成组件
上面的示例都是死的,封装成组件,就可以切换图片展示:
组件代码
ts
<template>
<div id="bottomImg" class="bottomImg" :style="{ height: imgHeigth, width: imgWidth, backgroundImage: 'url(' + props.bottomImg + ')' }">
<span class="imgLabel">{{ props.bottomLabel }}</span>
<div v-if="props.upperImg" class="upperImg" :style="{ backgroundImage: 'url(' + props.upperImg + ')', width: 100 - upperImgWidth + '%' }">
<span class="imgLabel">{{ props.upperLabel }}</span>
</div>
<div v-else class="upperUndefined" :style="{ width: 100 - upperImgWidth + '%' }">
<span class="undefinedSpan">暂无结果</span>
</div>
<span class="spanHandle" :style="{ left: 'calc(' + upperImgWidth + '% - 24px)' }"></span>
<input class="inputRange" type="range" v-model="upperImgWidth" min="0" max="100" />
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from "vue";
const imgHeigth = ref("0px");
const imgWidth = ref("0px");
const upperImgWidth = ref(50);
const props = defineProps({
bottomImg: {
type: String,
default: "",
},
upperImg: {
type: String,
default: "",
},
bottomLabel: {
type: String,
default: "原图",
},
upperLabel: {
type: String,
default: "效果图",
},
});
// 跟踪底层图片的变化,因为底层图片是基础
watch(
() => props.bottomImg,
() => {
getImgSize();
upperImgWidth.value = 50;
}
);
// 首次加载时初始化
onMounted(() => {
getImgSize();
});
function getImgSize() {
//加载图片获取图片真实宽度和高度
var image = new Image();
image.onload = function () {
imgHeigth.value = image.height + "px";
imgWidth.value = image.width + "px";
};
image.src = props.bottomImg; //img.src 应该是放在 onload 方法后边的, 因为当image的src发生改变,浏览器就会跑去加载这个src里的资源,先告诉浏览器图片加载完要怎么处理,再让它去加载图片
}
</script>
<style>
.bottomImg {
position: relative;
overflow: hidden;
}
.upperImg {
position: absolute;
top: 0;
right: 0;
height: 100%;
z-index: 1;
background-position: right top;
border-left: 2px solid rgb(255, 255, 255, 0.5);
}
.imgLabel {
font-size: 20px;
color: aliceblue;
text-shadow: 1px 1px #533d4a, 2px 2px #533d4a;
}
.upperUndefined {
position: absolute;
top: 0;
right: 0;
height: 100%;
z-index: 1;
font-size: 60px;
background-color: rgb(255, 255, 255, 0.8);
background-position: right top;
border-left: 2px solid rgb(255, 255, 255, 0.5);
}
.undefinedSpan {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
color: #999;
overflow: hidden;
}
.inputRange {
position: absolute;
height: 100%;
z-index: 3;
left: -4px;
touch-action: auto;
width: calc(100% + 4px);
opacity: 0;
}
.spanHandle {
position: absolute;
z-index: 2;
height: 48px;
width: 48px;
position: center;
font-size: 24px;
border: 1px;
border-radius: 50%;
top: calc(90% - 24px);
background-color: rgb(255, 255, 255, 0.5);
}
.spanHandle:before {
left: 5px;
transform: rotate(-45deg);
}
.spanHandle:after {
right: -5px;
transform: rotate(135deg);
}
.spanHandle:after,
.spanHandle:before {
border-left: 2px solid;
border-top: 2px solid;
content: "";
height: 10px;
position: absolute;
top: 50%;
transform-origin: 0 0;
width: 10px;
}
</style>
调用示例代码
ts
<template>
<div>
<div>
<button @click="changeBottomImg">切换底图</button>
<button @click="removeUpperImg">去除上层图</button>
<button @click="changeUpperImg">切换上层图</button>
</div>
<TwoImgCompare :bottom-img="bottomImg" bottom-label="原图" :upper-img="upperImg" upper-label="结果图"></TwoImgCompare>
</div>
</template>
<script lang="ts" setup>
import TwoImgCompare from "@/components/twoImgCompare.vue";
import { ref } from "vue";
const bottomImg = ref(new URL("@/images/bottomImg.jpg", import.meta.url).href); // 底图
const upperImg = ref(new URL("@/images/upperImg.jpg", import.meta.url).href); // 上层图
// 切换底图
const changeBottomImg = () => {
bottomImg.value = new URL("@/images/bottomImg2.jpg", import.meta.url).href;
};
// 去除上层图
const removeUpperImg = () => {
upperImg.value = "";
};
// 切换上层图
const changeUpperImg = () => {
upperImg.value = new URL("@/images/upperImg2.jpg", import.meta.url).href;
};
</script>
PS:开发该组件是用于自己开发的网站:极简AI工具箱,欢迎光临!