出现需求
需要在文档展示多张照片时,在移动端会竖直依此排列,影响阅读体验。
准备工作
首先,确认 vitepress 项目的组件导入方式:
在 .vitepress/theme/index.js 中注册组件 & 导入全局样式
js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
// 自定义组件导入
import ImgScroller from './components/ImgScroller.vue'
import './custom.css' // 可选:用于覆盖 theme 层级的全局样式
export default {
extends: DefaultTheme,
enhanceApp ({ app }) {
// 全局注册组件(在 markdown 中直接使用 <ImgScroller>)
app.component('ImgScroller', ImgScroller)
}
}说明:使用 extends: DefaultTheme + enhanceApp 可以在不重写整个主题的情况下注入全局组件和样式。VitePress+1
你也可以把 CSS 直接放到 ImgScroller.vue 中。
封装
创建组件:
vue
<template>
<div class="img-scroll-wrapper" ref="wrapper">
<!-- 提示框 -->
<div :class="['swipe-hint', { show: showHint }]">
<!-- slot 提示 -->
<slot name="hint" :type="hintType">
<!-- 默认提示文字(如果外部没定义 slot) -->
<span v-if="hintType === 'start'">👉 向右滑动查看更多图片</span>
<span v-else>这是最后一张图片了</span>
</slot>
</div>
<!-- 滑动容器 -->
<div class="img-scroll-inner" ref="scroller" @scroll="onScroll">
<slot />
</div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default {
name: 'ImgScroller',
props: {
autoHideMs: { type: Number, default: 2500 }, // 提示自动隐藏时间(ms)
mobileMaxWidth: { type: Number, default: 768 } // 认为是移动端的宽度阈值
},
setup(props) {
const scroller = ref(null)
const wrapper = ref(null)
const showHint = ref(false)
const hintType = ref('start') // 'start' | 'end'
let hideTimer = null
let showDelayTimer = null
let atEndHintShown = false
const isMobile = () => window.innerWidth <= props.mobileMaxWidth
// 显示提示框并自动隐藏
const startHintTimer = (type) => {
if (!isMobile()) return
clearTimeout(showDelayTimer)
clearTimeout(hideTimer)
hintType.value = type
// 先延迟1.5秒显示提示
showDelayTimer = setTimeout(() => {
showHint.value = true
// 显示后再等 autoHideMs 毫秒隐藏
hideTimer = setTimeout(() => {
showHint.value = false
}, props.autoHideMs)
}, 1500)
}
const isScrollAtEnd = () => {
if (!scroller.value) return false
const el = scroller.value
return el.scrollWidth - el.scrollLeft - el.clientWidth < 10
}
// onScroll 是绑定在滑动容器 .img-scroll-inner 的 @scroll 事件上的
// 当用户横向滚动时,它会被触发
const onScroll = () => {
if (isScrollAtEnd()) {
if (!atEndHintShown) {
startHintTimer('end')
atEndHintShown = true
}
} else {
atEndHintShown = false
}
}
onMounted(() => {
if (isMobile()) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
startHintTimer('start')
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (wrapper.value) {
observer.observe(wrapper.value)
}
}
})
onBeforeUnmount(() => {
clearTimeout(hideTimer)
clearTimeout(showDelayTimer)
})
return { scroller, wrapper, showHint, onScroll, hintType }
}
}
</script>
<style scoped>
/* 滑动容器样式 */
.img-scroll-inner {
height: 350px;
display: flex;
gap: 10px;
overflow-x: auto; /* 横向滚动 */
overflow-y: hidden; /* 隐藏垂直滚动条 */
scroll-snap-type: x mandatory; /* 可选:让滑动对齐 */
-webkit-overflow-scrolling: touch; /* iOS 惯性滑动 */
padding-bottom: 10px; /* 防止滚动条遮住内容 */
}
/* 图片样式 */
/* 穿透 slot 内 img 元素 */
::v-deep(.img-scroll-inner img) {
flex: 0 0 auto;
scroll-snap-align: start;
border-radius: 4px;
}
/* 隐藏滚动条(可选) */
/* .img-scroll::-webkit-scrollbar {
display: none;
} */
.img-scroll-wrapper {
position: relative; /* 新增,建立定位上下文 */
}
/* 优化的 Toast 样式提示 */
.swipe-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 80vw;
padding: 10px 20px;
background: rgba(58, 58, 58, 0.9);
color: white;
font-size: 16px;
border-radius: 24px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
pointer-events: none;
opacity: 0;
transition: opacity 1.2s ease;
z-index: 1000;
user-select: none;
white-space: nowrap;
}
.swipe-hint.show {
opacity: 1;
}
</style>其中:
html
<!-- 默认提示文字(如果外部没定义 slot) -->
<span v-if="hintType === 'start'">👉 向右滑动查看更多图片</span>
<span v-else>这是最后一张图片了</span>
</slot>它的作用是:
- 作为插槽的默认内容,当父组件没有传入名为
hint的具名插槽时,这部分内容就会显示。 - 也就是说,这相当于给插槽提供了一个“默认模板”,确保组件即使外部没有传自定义内容,也能正常显示提示文字。
详解判断是否滚动到末尾的逻辑
const isScrollAtEnd = () => {
if (!scroller.value) return false
const el = scroller.value
return el.scrollWidth - el.scrollLeft - el.clientWidth < 10
}拆解一下这段逻辑:
el.scrollWidth- 内容的总宽度(包括不可见的部分)。
el.scrollLeft- 当前已经水平滚动的距离。
el.clientWidth- 容器本身的可视宽度。
末尾判断公式
el.scrollWidth - el.scrollLeft - el.clientWidth- 这个值表示剩余可滚动的距离。
- 如果这个值 小于 10 像素(
< 10),就认为“已经到末尾”了。
为什么用 < 10 而不是 === 0?
- 在实际滚动中可能会出现小数或浮点计算误差(比如
0.0003或-0.5)。 - 不同浏览器和触控设备的滚动惯性也可能导致略微超出或不足末尾。
- 加一个容差(
< 10)可以让体验更稳定,避免误判。
✅ 结论isScrollAtEnd() 能够判断是否已经滚动到末尾(包括接近末尾的容差范围)。 所以它不仅能应对精确滚动,还能容忍微小的滚动误差,这就是它比较实用的原因。
使用
页面中的使用:
html
<ImgScroller>
<template #hint="{ type }">
<span v-if="type === 'start'">📌 自定义提示:向右滑动查看更多</span>
<span v-else>📌 自定义提示:这是最后一张了</span>
</template>
<!-- 如果图片因为路径错误、网络慢或被阻止而无法显示,浏览器会用 alt 内容替代显示。 -->
<img src="/xiangfa-京东农资招募.png" alt="A" />
<img src="/xiangfa-京东农资门店.png" alt="B" />
<img src="/xiangfa-京东农资招募1.png" alt="C" />
</ImgScroller>