两站图片滑动对比效果实现(VUE3)

像这种图片滑动对比的效果,网上还不少见吧,但是网上却不好找到完整现成的实现代码,我找到几个地方有类似的代码,但是都不好直接移植到代码里,因为很多都是使用原生html+css+js实现,太复杂了。反而不好应用到vue3中。

于是我借着他们的思路,自己实现了个。

前置条件

  1. 限制两张图片
  2. 图片大小必须一致,不一致会导致上层图片显示不全
  3. 底层图片必须存在,因为窗口的大小由他的大小决定

实现思路

  1. 将两张图片都看做是背景图,他们属于不同的层次
  2. 底层图片用相对定位,也就可以和其他元素一同正常展示,之后的所有元素都处于其包裹范围内,并且根据图片的大小确定整个样式的大小
  3. 上层图片用绝对定位,因为处于底层图片的包裹中,且大小是一样的,所以他们是完全重叠的,它的高度是和底层图片一致,但是宽度是可变的
  4. 利用input 的滑块模式 <input type="range" v-model="width" />来改变上层图片的宽度,这里也就是利用了vue3的响应式。这是比原生方式实现的简便之处
  5. 由于input的原生样式无法改变,所以得额外做个滑块来实现自定义样式,然后其位置也受width的控制,实际上并不是点击该滑块来滑动的
  6. input是出于最上层的,但是将其隐藏了,所以点击的时候看起来像是点击那个滑块,实际上点击的是input实现的滑动。
  7. 然后再修饰下其他细节,即可

接下来逐步分解出实现的效果,这样就更好理解文字的意思了

第一步,将两张图片分别呈现

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工具箱,欢迎光临!

相关推荐
ANnianStriver3 天前
PetLumina-AI 驱动的宠物生活管理平台
java·生活·vue3·springboot·ai编程·宠物·全栈开发
雨季mo浅忆4 天前
记录Vue3项目中的各类问题
前端·bug·vue3
八目蛛6 天前
八目蛛网络(免费工具网站导航)
css·vue.js·开源·vue3·html5·ai编程
颂love7 天前
Vue3基础入门
前端·学习·vue3
海市公约8 天前
Vue3组合式API中watch传值生命周期与自定义Hook实战
vue3·生命周期·watch·props·组件通信·defineexpose·自定义hook
海市公约8 天前
Vue3组合式API与响应式系统核心机制详解
vue3·computed·reactive·ref·响应式系统·composition api·script setup
小茴香3539 天前
Vue3路由权限动态管理
前端·前端框架·vue3
暗冰ཏོ13 天前
《2026 Vue2 + Vue3 完整学习指南:基础语法、路由缓存、登录拦截、项目实战与面试题》
前端·vue.js·vue·vue3·vue2
曲幽14 天前
写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了
vue3·web·vite·icon·element plus·vs code·import·unplugin
小云小白15 天前
若依-vue3 把深色版本改成天蓝色-含登录页
vue3·若依·天蓝色