hookehuyr

feat(Home): 实现流程步骤分页显示和响应式箭头定位

添加流程步骤分页功能,每屏最多显示7个步骤,支持横向滑动浏览
优化底部箭头在不同屏幕尺寸下的定位,添加窗口resize监听
使用占位项保持分页间距一致,改进移动端滚动体验
......@@ -36,14 +36,20 @@
<!-- 麻绳背景 -->
<div class="rope-background"></div>
<!-- 流程步骤 -->
<div class="process-steps" ref="processStepsRef">
<!-- 流程步骤:每屏最多显示7个,超出可横向滑动 -->
<div class="process-steps-wrapper" ref="processStepsRef">
<!-- 单页步骤容器:宽度占满一屏,Scroll Snap对齐 -->
<div
class="process-steps"
v-for="(page, pindex) in stepPages"
:key="pindex"
>
<div
v-for="(step, index) in processSteps"
:key="index"
v-for="(step, sindex) in page"
:key="pindex + '-' + sindex"
class="process-step"
:class="{ active: step.active }"
@click="selectStep(index)"
:class="{ active: step.active, placeholder: step.is_placeholder }"
@click="selectStep(pindex * steps_per_page + sindex)"
>
<!-- 顶部白色圆形占位(选中状态) -->
<div class="top-circle"></div>
......@@ -53,6 +59,7 @@
<span class="step-text">{{ step.name }}</span>
</div>
</div>
</div>
<!-- 全局底部箭头:跟随选中项移动 -->
<div v-show="processSteps.length" class="bottom-arrow moving-arrow" :style="arrowStyle">
<img src="https://cdn.ipadbiz.cn/stdj/images/home/%E7%82%B9@2x.png" alt="选中" />
......@@ -156,7 +163,7 @@
</template>
<script setup>
import { ref, computed, nextTick, onMounted, onActivated, watch } from 'vue'
import { ref, computed, nextTick, onMounted, onActivated, onUnmounted, watch } from 'vue'
import VideoPlayer from '@/components/VideoPlayer.vue'
import { useTitle } from '@vueuse/core';
import { useRouter } from 'vue-router'
......@@ -187,8 +194,44 @@ const videoOptions = ref({
const processSteps = ref([])
// 流程步骤容器引用
const processStepsRef = ref(null)
// 底部箭头的动态样式
const arrowStyle = ref({ left: '50%', bottom: '-1.25rem', transform: 'translateX(-50%)' })
// 底部箭头的动态样式(保持在容器内,避免被裁剪)
const arrowStyle = ref({ left: '50%', bottom: '0.25rem', transform: 'translateX(-50%)' })
// 底部箭头在不同屏幕下的rem偏移值
const arrowBottomRem = ref(0.25)
/**
* 处理窗口尺寸变化:更新箭头bottom并重新定位箭头
* @returns {void}
*/
const handleResize = () => {
updateArrowBottomByScreen()
updateArrowPosition()
}
// 每页显示的步骤数量(移动端要求最多7个)
const steps_per_page = 7
/**
* 计算分页后的步骤数组
* 说明:将流程步骤按每页7个分组,配合横向滚动实现分页显示
* @returns {Array<Array>} 分页后的步骤列表
*/
const stepPages = computed(() => {
const pages = []
const list = processSteps.value || []
for (let i = 0; i < list.length; i += steps_per_page) {
const page = list.slice(i, i + steps_per_page)
const pad = steps_per_page - page.length
if (pad > 0) {
for (let k = 0; k < pad; k++) {
// 占位项:用于固定每页7个槽位,保持间距一致
page.push({ id: `placeholder-${i}-${k}`, name: '', cover: '', active: false, is_placeholder: true })
}
}
pages.push(page)
}
return pages
})
// 获取当前选中的步骤
const currentStep = computed(() => {
......@@ -263,12 +306,28 @@ const selectStep = (index) => {
processSteps.value.forEach((step, i) => {
step.active = i === index
})
// 根据选中索引滚动到对应页
const pageIndex = Math.floor(index / steps_per_page)
scrollToPage(pageIndex)
// 切换步骤后重新计算装饰线位置
calculateLinePosition()
// 切换步骤后更新底部箭头位置
updateArrowPosition()
}
/**
* 滚动到指定分页
* 说明:容器按一屏宽度分页,横向滚动到目标页
* @param {number} pageIndex 目标页索引
*/
const scrollToPage = (pageIndex) => {
const container = processStepsRef.value
if (!container) return
const rect = container.getBoundingClientRect()
const pageWidth = rect.width
container.scrollTo({ left: pageIndex * pageWidth, behavior: 'smooth' })
}
// 三师七证数据列表
const mastersList = ref([])
......@@ -276,6 +335,11 @@ const mastersList = ref([])
onMounted(async () => {
// 进入页面时立即回到顶部
ensurePageScrollTop()
// 初始化不同屏幕下箭头bottom适配并监听resize
updateArrowBottomByScreen()
if (typeof window !== 'undefined') {
window.addEventListener('resize', handleResize)
}
await calculateLinePosition();
// 调用接口获取首页数据
const { code, list } = await homePageAPI();
......@@ -324,6 +388,13 @@ onActivated(() => {
ensurePageScrollTop()
})
// 组件卸载时清理resize监听
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', handleResize)
}
})
// 监听当前步骤变化,重新计算装饰线位置
watch(currentStep, () => {
calculateLinePosition()
......@@ -524,6 +595,10 @@ const handleNewsClick = (item) => {
/**
* 更新底部箭头的位置,使其移动到当前选中步骤下方
*/
/**
* 更新底部箭头的位置,使其移动到当前选中步骤下方
* @returns {void}
*/
const updateArrowPosition = () => {
const containerEl = processStepsRef.value
if (!containerEl) return
......@@ -542,10 +617,33 @@ const updateArrowPosition = () => {
// 使用px进行精确定位(移动端rem布局下,定位需要像素计算)
arrowStyle.value = {
left: `${centerX}px`,
bottom: '-1.25rem',
// 不同屏幕下适配底部偏移(rem单位)
bottom: `${arrowBottomRem.value}rem`,
transform: 'translateX(-50%)'
}
}
/**
* 依据屏幕宽度适配箭头bottom值
* 说明:遵循项目rem基准16px,将窗口宽度转换为rem做断点判断
* - ≤30rem(约≤480px):0.25rem
* - ≤48rem(约≤768px):0.3rem
* - >48rem:0.5rem
* @returns {void}
*/
const updateArrowBottomByScreen = () => {
let wRem = 48
if (typeof window !== 'undefined') {
wRem = (window.innerWidth || 768) / 16
}
if (wRem <= 30) {
arrowBottomRem.value = 0
} else if (wRem <= 48) {
arrowBottomRem.value = -0.25
} else {
arrowBottomRem.value = -0.5
}
}
</script>
<style scoped>
......@@ -598,7 +696,7 @@ const updateArrowPosition = () => {
/* 响应式设计 */
@media (max-width: 768px) {
.ceremony-process {
padding: 2rem 1rem;
padding: 2rem 1rem 0.25rem;
}
.ceremony-intro {
......@@ -957,13 +1055,38 @@ const updateArrowPosition = () => {
gap: 0.5rem;
}
/* 流程步骤分页容器:横向滚动,每屏一页 */
.process-steps-wrapper {
position: relative;
width: 100%;
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
/* 底部留白,确保容器高度包含箭头展示空间 */
padding-top: 0.5rem;
padding-bottom: 1rem;
}
/* 隐藏移动端滚动条以优化视觉 */
.process-steps-wrapper::-webkit-scrollbar {
display: none;
}
/* 单页占满一屏,滚动对齐 */
.process-steps-wrapper .process-steps {
flex: 0 0 100%;
scroll-snap-align: start;
}
/* 单个流程步骤 */
.process-step {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
/* 固定占位,避免因数量不足导致单页拉伸 */
flex: 0 0 auto;
cursor: pointer;
transition: transform 0.2s ease;
}
......@@ -1043,6 +1166,16 @@ const updateArrowPosition = () => {
pointer-events: none;
}
/* 占位项:隐藏内容但占据空间,保持间距统一 */
.process-step.placeholder .step-content {
visibility: hidden;
pointer-events: none;
}
.process-step.placeholder .top-circle {
visibility: hidden;
}
/* 响应式调整 */
@media (max-width: 48rem) {
.ceremony-process {
......@@ -1077,7 +1210,7 @@ const updateArrowPosition = () => {
@media (max-width: 30rem) {
.ceremony-process {
padding: 2rem 0.25rem;
padding: 2rem 1rem 0.5rem;
}
.ceremony-title img {
......