前言
今年四月份新入职了现在的单位,大概是在五月份左右接到了一个新需求,需要做一个模拟橱窗效果,并支持类似手势的拖动效果,对于我这个常年写后台管理页面的人来说,哇哦,我得研究一下了 😂,找到了之前做过的例子仔细研究了一下,发现好像也不难(
vue3
项目)。
需求细节:
首屏默认展示6个橱窗,超过的需要隐藏,当橱窗向左或者向右移动后,超出可视区的橱窗隐藏
移动分为三种:
- 点击最左侧或者最右侧箭头,可实现向左或者向右移动多个橱窗,当滚动条已经到头了,箭头置灰;
- 点击非当前橱窗的左右某一个橱窗,可向左或者向右移动,使整个橱窗以当前点击橱窗为中心居中;
- 鼠标点击某一个橱窗,不松开,向右或者向右移动,松开鼠标后,可实现向左或者向右移动,类似touch 左右滑动效果;
问题一:如何实现居中?
scrollIntoView
修改滚动条到指定位置,让它以某种对齐方式对其,并可设置滚动行为方式
适用于在某个时机需要手动改变滚动条的位置,且移动的是一个列表,移动滚动条后,使其以某个元素为目标,执行对应的滚动标准。
首屏的样子:
当点击第6
张后,以第6
张为中心居中:
场景看过了,现在我们再来看一下scrollIntoView
都有哪些属性值?
behavior
用于指定滚动的行为方式
属性 | 属性值 | 描述 |
---|---|---|
behavior | auto (默认) | 浏览器的滚动行为可能是立即完成的,也可能是平滑的,具体取决于浏览器的实现。 |
- | smooth | 浏览器将使用平滑的过渡效果来滚动元素。这将会使滚动看起来更流畅,并且可以通过添加动画来控制滚动的速度和缓动效果。 |
- | instant | 浏览器将立即滚动元素,没有任何过渡效果。这相当于立即跳转到元素可见的位置,没有平滑的滚动效果。 |
block
:用于指定垂直方向上的对齐方式
属性 | 属性值 | 描述 |
---|---|---|
block | start (默认) | 将元素的顶部边缘与容器的顶部边缘对齐。 |
- | center | 将元素垂直居中对齐于容器。 |
- | end | 将元素的底部边缘与容器的底部边缘对齐。 |
- | nearest | 根据元素的当前位置和可见区域来选择最接近的对齐方式。如果元素已经完全可见,则与 "start" 相同;如果元素在视图中部分可见,则与 "center" 相同;如果元素完全不可见,则与 "end" 相同。 |
inline
属性用于指定水平方向上的对齐方式
属性 | 属性值 | 描述 |
---|---|---|
block | start (默认) | 将元素的左边缘与容器的左边缘对齐。 |
- | center | 将元素水平居中对齐于容器。 |
- | end | 将元素的右边缘与容器的右边缘对齐。 |
- | nearest | 根据元素的当前位置和可见区域来选择最接近的对齐方式。如果元素已经完全可见,则与 "start" 相同;如果元素在视图中部分可见,则与 "center" 相同;如果元素完全不可见,则与 "end" 相同。 |
课外知识: behavior: 'smooth'
这个也可以使用css
样式来实现过渡效果:
css
scroll-behavior: smooth;
看过示例,现在我们上代码:
xml
...
<Row>
<!-- 目录 -->
<div
ref="scrollingPanel"
>
<div
v-for="(slide, index) in slides"
:key="slide.id"
ref="thumbnnailElement"
@click="turnSlide(index)"
>
<div
:slide="slide"
class="thumbnail"
/>
<div class="slide-index" :class="{ active: index === slideIndex }">
幻灯片{{ index + 1 }}
</div>
</div>
</div>
</Row>
...
<script lang="ts" setup>
import {ref} from 'vue';
const thumbnnailElement = ref();
// 跳转到指定元素,并处理点击时,使当前选中元素在可视区内并水平居中
const turnSlide = (index: number) => {
thumbnnailElement?.value[index]?.scrollIntoView({
inline: 'center',
});
};
</script>
在同一个项目中,另一个需求需要一个竖向列表
,仍然要求每次打开后目录以选中的幻灯片居中的形式展示,如下是初始化的样子。
当点击后执行垂直居中
代码如下:
ini
...
<ThumbnailPopover
@close="emit('close')"
>
<div class="thumbnailbox">
<div
v-for="(slide, index) in slides"
ref="thumbnnailElement"
:key="slide.id"
@click="turnSlide(index)"
>
<Tooltip
:mouseLeaveDelay="0"
:mouseEnterDelay="0.5"
:title="slide.title"
>
<div class="thumbnail-title">
{{ slide.title }}
</div>
</Tooltip>
<Row>
<div class="font" :class="{ active: index === slideIndex }">
{{ index + 1 }}
</div>
<div
:slide="slide"
:visible="index < slidesLoadLimit"
class="thumbnail"
:class="{ active: index === slideIndex }"
/>
</Row>
</div>
</div>
</ThumbnailPopover>
...
<scrip lang="ts" setup>
import {ref , watch} from 'vue';
const thumbnnailElement = ref();
const turnSlide = (index: number) => {
props.turnSlideToIndex(index);
emit('close');
};
// 判断目录显示时去移动滚动条使其居中
watch(slideThumbnailModelVisible, (value) => {
if (value) {
thumbnnailElement?.value[slideIndex.value]?.scrollIntoView({
block: 'center',
});
}
});
</script>
我们还可以设置在可视区从start
的位置显示,我就拿上面的这个例子给大家试试:
scss
watch(slideThumbnailModelVisible, (value) => {
if (value) {
thumbnnailElement?.value[slideIndex.value]?.scrollIntoView({
block: 'start', // 代码只需要改这里
});
}
});
它就会在可视区顶部展示:
底部展示也是一样的,只是我当前的例子中有一个关闭按钮挡住了,哈哈哈😂
问题二:怎么计算滚动条是否滚动到底部,或者是否在顶部的位置?
ini
...
<div ref="scrollingPanel">box</div>
...
const scrollingPanel = ref();
const isLeftmost = ref<boolean>(true); // 是否在最左侧
const isRightmost = ref<boolean>(false); // 是否在最右侧
const handelScroll = () => {
const currentScrollLeft = scrollingPanel.value?.scrollLeft;
const rightmost =
currentScrollLeft + scrollingPanel.value?.clientWidth >=
scrollingPanel.value?.scrollWidth;
isLeftmost.value = currentScrollLeft <= 0;
isRightmost.value = rightmost && currentScrollLeft > 0;
};
问题三:在web
模拟touch
向左滑动展示效果
后期我去补个视频图
需求:在列表上点击左键,然后向一个方向移动,然后松开左键后,列表视图会跟随刚刚移动的方向滚动列表。
ini
<!-- 目录 -->
<div
ref="scrollingPanel"
class="page-view"
@mousemove="handleMouseMove"
@touchmove="handleMouseMove"
>
<div
v-for="(slide, index) in slides"
:key="slide.id"
ref="thumbnnailElement"
class="item-box"
:class="{ active: index === slideIndex }"
@click="turnSlide(index)"
@mousedown="($event) => handleMouseDown($event)"
@touchstart="($event) => handleMouseDown($event)"
>
<ThumbnailSlide
:slide="slide"
:size="224"
:visible="index < slidesLoadLimit"
class="thumbnail"
/>
<div class="slide-index" :class="{ active: index === slideIndex }">
幻灯片{{ index + 1 }}
</div>
</div>
</div>
css
布局: 固定列表的宽高,隐藏除了宽高之外的内容,横向展现:
css
.page-view {
width: 1630px;
height: auto;
position: absolute;
top: 24px;
left: 152px;
display: flex;
overflow: hidden;
box-sizing: content-box;
padding: 3px;
cursor: pointer;
scroll-behavior: smooth;
}
.item-box {
height: 195px;
background: #0f2a55;
box-shadow: inset 0 0 10px 0 #335986;
border: 1px solid #999999;
margin-left: 16px;
&:first-child {
margin-left: 0;
}
&.active {
background: #0f2a55;
outline: 3px solid #00fcec;
}
.thumbnail {
width: 224px;
height: 126px;
border: 1px solid yellow;
margin: 15px 16px;
}
.slide-index {
font-size: 20px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #cccccc;
margin-top: 20px;
text-align: center;
user-select: none;
}
}
js
来操作处理按下 抬起和移动时的位置:
ini
const thumbnnailElement = ref();
const scrollingPanel = ref();
const startX = ref<number>(0); // 鼠标按下时的 x 坐标
const isMoveTime = ref<boolean>(false);
const dragging = ref<boolean>(false); // 是否正在拖动
// 鼠标按下
const handleMouseDown = (event: MouseEvent | TouchEvent) => {
dragging.value = true;
const clientX =
'clientX' in event ? event.clientX : event.changedTouches[0].clientX;
startX.value = clientX;
};
// 鼠标抬起
const handleMouseUp = () => {
dragging.value = false;
setTimeout(() => {
isMoveTime.value = false;
}, 0);
};
// 开始移动
const handleMouseMove = (event: MouseEvent | TouchEvent) => {
event.preventDefault();
event.stopPropagation();
const currentElementPosition = scrollingPanel.value?.scrollLeft;
// 计算拖动距离
if (dragging.value) {
isMoveTime.value = true;
const clientX =
'clientX' in event ? event.clientX : event.changedTouches[0].clientX;
const distance = currentElementPosition - (clientX - startX.value) * 5;
scrollingPanel?.value.scrollTo({
left: distance,
top: 0,
});
}
};
结尾
这个需求里还有很多业务的细节,无关技术,在这里就不再赘述了,其实过程中也走了一些弯路,例如这个scrollIntoView
方法,我这么多年第一次遇到,在这之前还自写了一套居中的逻辑,一堆计算,哈哈哈哈,都过去了。感觉通过这个需求,我好像对css
又产生了兴趣,多年不写css
,现在仍然能写出来,感谢自己的脑袋,脑子是个好东西啊,以后要多多动脑了!如果有机会,后面会出一些css
相关的文章,同样给不常写css
,常年写后台的小伙伴,hhhh,相互鼓励吧!