<template>
<div class="container">
<!-- 大盒子 -->
<div class="father_box">
<!-- 滚动盒子 -->
<div class="scroll_box" ref="scrollBox" @mousedown="mousedown" @mousemove="mousemove" @mouseup="mouseup"
@mouseleave="mouseleave">
<div class="scroll_box_item" :class="{'scroll_box_item_active':index == item}" ref="scrollBoxItem" @click="change(item)" v-for="(item) in 20" :key="item">{{ item }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref,onMounted } from "vue";
let scrollBox = ref();
let scrollBoxItem = ref();
let index = ref(0);
let maxNum = ref(0); //最大移动位置
let upFlag = ref(false); //鼠标是否弹起,用于判断是否执行mouseleave的代码
let isMove = ref(false); //控制图片是否能执行选中代码,避免移动的时候,会选中图片 true执行选中代码,false不执行,在mousedown时设置为true,mousemove设置为false,这样就能分辨出当前是移动还是点击
let flag = ref(false); // 鼠标按下
let downX = ref(0); // 鼠标点击的x下标
let scrollLeft = ref(0); // 当前鼠标移动距离
let leftOld = ref(0); //上一次偏移量
const change = (item:any) =>{
if(!isMove.value){ //如果当前是在移动,那就不执行点击事件的代码,isMove值会在mouseup和mouseleave时,赋值为false
index.value = item;
}
}
const mousedown = (e: any) => {
isMove.value = false;
flag.value = true;
downX.value = e.clientX; // 获取到点击的x下标
};
const mousemove = (e: any) => {
if (flag.value) { //只有鼠标按下了,才能执行滑动
isMove.value = true;
upFlag.value = false;
// 判断是否是鼠标按下滚动元素区域
// 获取移动的x轴
var moveX = e.clientX;
// 当前移动的x轴下标减去刚点击下去的x轴下标得到鼠标滑动距离
scrollLeft.value = moveX - downX.value;
// 上一次移动距离减去当前鼠标的滑动距离
scrollBox.value.style.transform = `translateX(${leftOld.value + scrollLeft.value
}px)`;
}
};
// 鼠标抬起时,不执行mousemove
const mouseup = () => {
upFlag.value = true;
flag.value = false;
if (isMove.value) { //必须有移动才能执行这里,不然只点击然后鼠标弹起都执行的话,这个移动距离就会一直加
leftOld.value += scrollLeft.value; //记录上一次的鼠标偏移距离
}
if (leftOld.value > 0) {
scrollBox.value.style.transform = `translateX(0px)`;
leftOld.value = 0;
} else if (leftOld.value < maxNum.value) {
scrollBox.value.style.transform = `translateX(${maxNum.value}px)`;
leftOld.value = maxNum.value;
}
};
// 鼠标离开元素,不执行mousemove
const mouseleave = () => {
if (!upFlag.value && isMove.value) { //鼠标没有弹起并且有移动才能执行这里
leftOld.value += scrollLeft.value; //记录上一次的鼠标偏移距离
if (leftOld.value > 0) { //leftOld如果大于0了,那就是盒子往右移动了,那就必须复原,这里的往右移动和往右滑动不一样,
scrollBox.value.style.transform = `translateX(0px)`;
leftOld.value = 0;
} else if (leftOld.value < maxNum.value) {
scrollBox.value.style.transform = `translateX(${maxNum.value}px)`;
leftOld.value = maxNum.value;
}
}
isMove.value = false;
upFlag.value = false;
flag.value = false;
};
onMounted(() =>{
// 这个20就是盒子的个数,scrollBoxItem.value[0].offsetWidth是盒子的宽度和margin之和,这个距离慢慢调整
maxNum.value = -((20 - 8) * scrollBoxItem.value[0].offsetWidth);
});
</script>
<style lang="scss" scoped>
.father_box {
width: 100%;
overflow: hidden;
user-select: none; //字体不能被选中
.scroll_box {
display: inline-block; //不会继承父元素的宽高
white-space: nowrap; //子元素不换行
.scroll_box_item {
width: 200px;
height: 150px;
background-color: red;
display: inline-block;
cursor: pointer;
margin-right: 10px;
text-align: center;
line-height: 150px;
font-size: 20px;
color: #fff;
}
.scroll_box_item_active{
background-color: black;
}
}
}
</style>
整体思路:一共有三层,第一层是最外层盒子,固定宽度,超出隐藏,第二层盒子宽高由子盒子决定,然后移动盒子来实现滑动,第三层就是子盒子咯。首先第二层盒子上面绑定mousedown
,mousemove
,mouseup
,mouseleave
事件,当鼠标mousedown
时,记录当前鼠标点击的位置(downX)
,然后mousemove
的时候获取鼠标的位置(scrollLeft)
,通过这两个位置来获取鼠标移动的距离,然后上一次的移动距离(leftOld)
+ 这次的移动距离(scrollLeft)
,然后scrollBox.value.style.transform = translateX(${leftOld.value + scrollLeft.value }px)
,来移动第二层盒子,再加上一些判断就ok了
这里有一个小瑕疵,就是刷新页面后,直接点击子元素,会执行一次mousemove,导致选不中子元素,但是先点击一次空白地方,就不会执行mousemove了,可能是浏览器的安全策略,但是我没听过