feat(首页): 添加流程步骤底部箭头跟随效果和轮播触摸滑动功能
- 将底部箭头改为全局组件并实现跟随当前选中步骤移动 - 为新闻轮播添加触摸滑动切换功能,支持左右滑动切换 - 移除路由中未使用的滚动行为配置 - 优化轮播容器触摸事件处理,避免与垂直滚动冲突
Showing
2 changed files
with
129 additions
and
15 deletions
| 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 { | ... | ... |
-
Please register or login to post a comment