hookehuyr

feat(首页): 添加流程步骤底部箭头跟随效果和轮播触摸滑动功能

- 将底部箭头改为全局组件并实现跟随当前选中步骤移动
- 为新闻轮播添加触摸滑动切换功能,支持左右滑动切换
- 移除路由中未使用的滚动行为配置
- 优化轮播容器触摸事件处理,避免与垂直滚动冲突
1 /* 1 /*
2 * @Date: 2025-10-30 10:29:15 2 * @Date: 2025-10-30 10:29:15
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-04 10:53:52 4 + * @LastEditTime: 2025-11-04 20:35:35
5 * @FilePath: /stdj_h5/src/router/index.js 5 * @FilePath: /stdj_h5/src/router/index.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -53,13 +53,6 @@ const routes = [ ...@@ -53,13 +53,6 @@ const routes = [
53 const router = createRouter({ 53 const router = createRouter({
54 history: createWebHashHistory('/index.html'), 54 history: createWebHashHistory('/index.html'),
55 routes, 55 routes,
56 - scrollBehavior(to, from, savedPosition) {
57 - if (savedPosition) {
58 - return savedPosition
59 - } else {
60 - return { top: 0 }
61 - }
62 - }
63 }) 56 })
64 57
65 // 路由守卫 58 // 路由守卫
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
37 <div class="rope-background"></div> 37 <div class="rope-background"></div>
38 38
39 <!-- 流程步骤 --> 39 <!-- 流程步骤 -->
40 - <div class="process-steps"> 40 + <div class="process-steps" ref="processStepsRef">
41 <div 41 <div
42 v-for="(step, index) in processSteps" 42 v-for="(step, index) in processSteps"
43 :key="index" 43 :key="index"
...@@ -52,11 +52,10 @@ ...@@ -52,11 +52,10 @@
52 <div class="step-content"> 52 <div class="step-content">
53 <span class="step-text">{{ step.name }}</span> 53 <span class="step-text">{{ step.name }}</span>
54 </div> 54 </div>
55 -
56 - <!-- 底部箭头(选中状态) -->
57 - <div v-if="step.active" class="bottom-arrow">
58 - <img src="https://cdn.ipadbiz.cn/stdj/images/home/%E7%82%B9@2x.png" alt="选中" />
59 </div> 55 </div>
56 + <!-- 全局底部箭头:跟随选中项移动 -->
57 + <div v-show="processSteps.length" class="bottom-arrow moving-arrow" :style="arrowStyle">
58 + <img src="https://cdn.ipadbiz.cn/stdj/images/home/%E7%82%B9@2x.png" alt="选中" />
60 </div> 59 </div>
61 </div> 60 </div>
62 </div> 61 </div>
...@@ -116,7 +115,13 @@ ...@@ -116,7 +115,13 @@
116 </div> 115 </div>
117 116
118 <!-- 轮播容器 --> 117 <!-- 轮播容器 -->
119 - <div class="news-carousel-container"> 118 + <div
119 + class="news-carousel-container"
120 + ref="newsCarouselContainer"
121 + @touchstart="onTouchStart"
122 + @touchmove="onTouchMove"
123 + @touchend="onTouchEnd"
124 + >
120 <div class="news-carousel" ref="newsCarousel"> 125 <div class="news-carousel" ref="newsCarousel">
121 <div class="news-item" v-for="(item, index) in displayNewsItems" :key="index" @click="handleNewsClick(item)"> 126 <div class="news-item" v-for="(item, index) in displayNewsItems" :key="index" @click="handleNewsClick(item)">
122 <div class="news-image"> 127 <div class="news-image">
...@@ -180,6 +185,10 @@ const videoOptions = ref({ ...@@ -180,6 +185,10 @@ const videoOptions = ref({
180 185
181 // 法会流程步骤数据 186 // 法会流程步骤数据
182 const processSteps = ref([]) 187 const processSteps = ref([])
188 +// 流程步骤容器引用
189 +const processStepsRef = ref(null)
190 +// 底部箭头的动态样式
191 +const arrowStyle = ref({ left: '50%', bottom: '-1.25rem', transform: 'translateX(-50%)' })
183 192
184 // 获取当前选中的步骤 193 // 获取当前选中的步骤
185 const currentStep = computed(() => { 194 const currentStep = computed(() => {
...@@ -228,13 +237,18 @@ const calculateLinePosition = async () => { ...@@ -228,13 +237,18 @@ const calculateLinePosition = async () => {
228 } 237 }
229 } 238 }
230 239
231 -// 点击切换步骤状态 240 +/**
241 + * 点击切换步骤状态并更新底部箭头位置
242 + * @param {number} index 选中步骤索引
243 + */
232 const selectStep = (index) => { 244 const selectStep = (index) => {
233 processSteps.value.forEach((step, i) => { 245 processSteps.value.forEach((step, i) => {
234 step.active = i === index 246 step.active = i === index
235 }) 247 })
236 // 切换步骤后重新计算装饰线位置 248 // 切换步骤后重新计算装饰线位置
237 calculateLinePosition() 249 calculateLinePosition()
250 + // 切换步骤后更新底部箭头位置
251 + updateArrowPosition()
238 } 252 }
239 253
240 // 三师七证数据列表 254 // 三师七证数据列表
...@@ -277,6 +291,8 @@ onMounted(async () => { ...@@ -277,6 +291,8 @@ onMounted(async () => {
277 // 初始化轮播位置 291 // 初始化轮播位置
278 nextTick(() => { 292 nextTick(() => {
279 updateCarouselPosition(false) 293 updateCarouselPosition(false)
294 + // 初始化底部箭头位置
295 + updateArrowPosition()
280 }) 296 })
281 } 297 }
282 }) 298 })
...@@ -284,6 +300,7 @@ onMounted(async () => { ...@@ -284,6 +300,7 @@ onMounted(async () => {
284 // 监听当前步骤变化,重新计算装饰线位置 300 // 监听当前步骤变化,重新计算装饰线位置
285 watch(currentStep, () => { 301 watch(currentStep, () => {
286 calculateLinePosition() 302 calculateLinePosition()
303 + updateArrowPosition()
287 }, { deep: true }) 304 }, { deep: true })
288 305
289 // 查看更多按钮点击事件 306 // 查看更多按钮点击事件
...@@ -329,6 +346,15 @@ const viewMore = (type, item) => { ...@@ -329,6 +346,15 @@ const viewMore = (type, item) => {
329 const newsCarousel = ref(null) 346 const newsCarousel = ref(null)
330 const currentSlide = ref(1) // 从1开始,因为0是克隆的最后一项 347 const currentSlide = ref(1) // 从1开始,因为0是克隆的最后一项
331 const isTransitioning = ref(false) 348 const isTransitioning = ref(false)
349 +// 轮播容器引用(用于触摸滑动)
350 +const newsCarouselContainer = ref(null)
351 +// 触摸滑动相关状态
352 +const touchStartX = ref(0)
353 +const touchStartY = ref(0)
354 +const touchDeltaX = ref(0)
355 +const touchDeltaY = ref(0)
356 +const isSwiping = ref(false)
357 +const swipeThreshold = 30 // 滑动触发阈值,单位px
332 358
333 // 新闻数据 359 // 新闻数据
334 const newsItems = ref([]) 360 const newsItems = ref([])
...@@ -400,6 +426,64 @@ const updateCarouselPosition = (animated = true) => { ...@@ -400,6 +426,64 @@ const updateCarouselPosition = (animated = true) => {
400 carouselEl.style.transform = `translateX(-${distance}px)` 426 carouselEl.style.transform = `translateX(-${distance}px)`
401 } 427 }
402 428
429 +/**
430 + * 触摸开始事件:记录初始触摸点
431 + * @param {TouchEvent} e 触摸事件对象
432 + */
433 +const onTouchStart = (e) => {
434 + const t = e.touches && e.touches[0]
435 + if (!t) return
436 + touchStartX.value = t.clientX
437 + touchStartY.value = t.clientY
438 + touchDeltaX.value = 0
439 + touchDeltaY.value = 0
440 + isSwiping.value = false
441 +}
442 +
443 +/**
444 + * 触摸移动事件:计算水平与垂直位移,判断是否为水平滑动
445 + * @param {TouchEvent} e 触摸事件对象
446 + */
447 +const onTouchMove = (e) => {
448 + const t = e.touches && e.touches[0]
449 + if (!t) return
450 + touchDeltaX.value = t.clientX - touchStartX.value
451 + touchDeltaY.value = t.clientY - touchStartY.value
452 + // 以水平位移为主且超过轻微阈值时,认定为滑动
453 + if (Math.abs(touchDeltaX.value) > Math.abs(touchDeltaY.value) && Math.abs(touchDeltaX.value) > 10) {
454 + isSwiping.value = true
455 + // 阻止默认滚动,避免与页面垂直滚动冲突
456 + e.preventDefault()
457 + } else {
458 + isSwiping.value = false
459 + }
460 +}
461 +
462 +/**
463 + * 触摸结束事件:根据滑动距离与方向触发上一张/下一张
464 + */
465 +const onTouchEnd = () => {
466 + if (!isSwiping.value) return
467 + if (isTransitioning.value) return
468 +
469 + if (Math.abs(touchDeltaX.value) >= swipeThreshold) {
470 + if (touchDeltaX.value < 0) {
471 + // 左滑,下一张
472 + nextSlide()
473 + } else {
474 + // 右滑,上一张
475 + prevSlide()
476 + }
477 + }
478 +
479 + // 重置状态
480 + touchStartX.value = 0
481 + touchStartY.value = 0
482 + touchDeltaX.value = 0
483 + touchDeltaY.value = 0
484 + isSwiping.value = false
485 +}
486 +
403 // 处理新闻点击事件 487 // 处理新闻点击事件
404 const handleNewsClick = (item) => { 488 const handleNewsClick = (item) => {
405 // 这里可以添加跳转到新闻详情页面的逻辑 489 // 这里可以添加跳转到新闻详情页面的逻辑
...@@ -409,6 +493,32 @@ const handleNewsClick = (item) => { ...@@ -409,6 +493,32 @@ const handleNewsClick = (item) => {
409 router.push({ name: 'NewsDetail', params: { id: item.id } }) 493 router.push({ name: 'NewsDetail', params: { id: item.id } })
410 } 494 }
411 } 495 }
496 +
497 +/**
498 + * 更新底部箭头的位置,使其移动到当前选中步骤下方
499 + */
500 +const updateArrowPosition = () => {
501 + const containerEl = processStepsRef.value
502 + if (!containerEl) return
503 +
504 + const stepsEls = containerEl.querySelectorAll('.process-step')
505 + const activeIndex = processSteps.value.findIndex(s => s.active)
506 + if (activeIndex < 0 || !stepsEls[activeIndex]) return
507 +
508 + const activeEl = stepsEls[activeIndex]
509 + const containerRect = containerEl.getBoundingClientRect()
510 + const activeRect = activeEl.getBoundingClientRect()
511 +
512 + // 计算选中项中心点相对容器的left位置
513 + const centerX = activeRect.left + activeRect.width / 2 - containerRect.left
514 +
515 + // 使用px进行精确定位(移动端rem布局下,定位需要像素计算)
516 + arrowStyle.value = {
517 + left: `${centerX}px`,
518 + bottom: '-1.25rem',
519 + transform: 'translateX(-50%)'
520 + }
521 +}
412 </script> 522 </script>
413 523
414 <style scoped> 524 <style scoped>
...@@ -895,6 +1005,15 @@ const handleNewsClick = (item) => { ...@@ -895,6 +1005,15 @@ const handleNewsClick = (item) => {
895 .bottom-arrow img { 1005 .bottom-arrow img {
896 width: 1rem; 1006 width: 1rem;
897 height: 1rem; 1007 height: 1rem;
1008 + /* 底部箭头左右位移动画 */
1009 + animation: arrowHSlide 1.8s ease-in-out infinite;
1010 + will-change: transform;
1011 +}
1012 +
1013 +/* 可移动箭头样式:启用left过渡,实现平滑跟随 */
1014 +.moving-arrow {
1015 + transition: left 0.3s ease;
1016 + pointer-events: none;
898 } 1017 }
899 1018
900 /* 响应式调整 */ 1019 /* 响应式调整 */
...@@ -1228,6 +1347,8 @@ const handleNewsClick = (item) => { ...@@ -1228,6 +1347,8 @@ const handleNewsClick = (item) => {
1228 overflow: hidden; 1347 overflow: hidden;
1229 margin-top: 2rem; 1348 margin-top: 2rem;
1230 z-index: 2; /* 确保轮播容器在背景图片之上 */ 1349 z-index: 2; /* 确保轮播容器在背景图片之上 */
1350 + /* 仅允许垂直滚动,优化左右滑动体验 */
1351 + touch-action: pan-y;
1231 } 1352 }
1232 1353
1233 .news-carousel { 1354 .news-carousel {
......