记录实现一个橱窗相册,支持点击居中,touch左右滑动效果

前言

今年四月份新入职了现在的单位,大概是在五月份左右接到了一个新需求,需要做一个模拟橱窗效果,并支持类似手势的拖动效果,对于我这个常年写后台管理页面的人来说,哇哦,我得研究一下了 😂,找到了之前做过的例子仔细研究了一下,发现好像也不难(vue3项目)。

需求细节:

首屏默认展示6个橱窗,超过的需要隐藏,当橱窗向左或者向右移动后,超出可视区的橱窗隐藏

移动分为三种:

  1. 点击最左侧或者最右侧箭头,可实现向左或者向右移动多个橱窗,当滚动条已经到头了,箭头置灰;
  2. 点击非当前橱窗的左右某一个橱窗,可向左或者向右移动,使整个橱窗以当前点击橱窗为中心居中;
  3. 鼠标点击某一个橱窗,不松开,向右或者向右移动,松开鼠标后,可实现向左或者向右移动,类似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,相互鼓励吧!

相关推荐
光影少年4 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_5 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu10830189118 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾9 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking9 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu11 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym16 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫17 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫21 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat22 分钟前
前端性能优化2
前端