You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
4.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<scroll-view
ref="scrollRef"
class="scroll-container"
:scroll-x="direction === 'x'"
:scroll-y="direction === 'y'"
:scroll-with-animation="true"
:show-scrollbar="false"
:scroll-into-view="targetId"
@scroll="handleScroll"
:enable-flex="direction === 'x'?true:false"
@touchmove.prevent="handleTouchMove"
style="touch-action: none;"
>
<slot></slot>
</scroll-view>
</template>
<script setup>
import { ref, onMounted, nextTick, watch } from 'vue';
const props = defineProps({
// 滚动方向 'x' 或 'y'
direction: {
type: String,
default: 'y',
validator: (val) => ['x', 'y'].includes(val)
},
// 内容项数量
itemCount: {
type: Number,
required: true
},
// 当前激活的索引
activeIndex: {
type: Number,
default: 0
},
// 每个内容项的选择器,如 ".list-item"
itemSelector: {
type: String,
required: true
},
// 滚动触发索引变化的阈值比例0-1
thresholdRatio: {
type: Number,
default: 0.3
}
});
const emits = defineEmits(['update:activeIndex']);
const scrollRef = ref(null);
const targetId = ref(null);
const itemPositions = ref([]); // 存储每个项的位置信息
const scrollViewInfo = ref(null); // 滚动容器信息
const handleTouchMove = (e)=> {
e.preventDefault(); // 强制阻止默认滚动行为
return false; // 增强阻止效果
}
// 获取元素信息的工具函数
const getElementsInfo = (selector) => {
return new Promise(resolve => {
uni.createSelectorQuery()
.selectAll(selector)
.boundingClientRect(resolve)
.exec();
});
};
// 获取滚动容器信息
const getScrollViewInfo = () => {
return new Promise(resolve => {
uni.createSelectorQuery()
.select('.scroll-container')
.boundingClientRect(resolve)
.exec();
});
};
// 初始化每个项的位置信息
const initItemPositions = async () => {
await nextTick();
// 获取滚动容器信息
scrollViewInfo.value = await getScrollViewInfo();
console.log("test")
if (!scrollViewInfo.value) return;
// 获取所有内容项信息
const itemsInfo = await getElementsInfo(props.itemSelector);
if (!itemsInfo || itemsInfo.length === 0) return;
// 计算每个项相对于滚动容器的位置
itemPositions.value = itemsInfo.map((item, index) => {
const scrollKey = props.direction === 'x' ? 'left' : 'top';
const sizeKey = props.direction === 'x' ? 'width' : 'height';
return {
index,
start: item[scrollKey] - scrollViewInfo.value[scrollKey],
end: item[scrollKey] - scrollViewInfo.value[scrollKey] + item[sizeKey],
size: item[sizeKey]
};
});
};
// 根据索引滚动到对应位置
const scrollToIndex = (index) => {
if (index < 0 || index >= props.itemCount) return;
targetId.value = `${props.itemSelector.replace('.', '')}-${index}`;
// 重置targetId避免重复点击不生效
setTimeout(() => {
targetId.value = null;
}, 500);
};
// 处理滚动事件
const handleScroll = (e) => {
if (!itemPositions.value.length || !scrollViewInfo.value) return;
const scrollKey = props.direction === 'x' ? 'scrollLeft' : 'scrollTop';
const scrollPos = e.detail[scrollKey];
const containerSize = props.direction === 'x'
? scrollViewInfo.value.width
: scrollViewInfo.value.height;
// 计算当前应该激活的索引
for (let i = 0; i < itemPositions.value.length; i++) {
const item = itemPositions.value[i];
const threshold = item.size * props.thresholdRatio;
// 判断当前滚动位置是否超过项的阈值
if (scrollPos >= (item.start - threshold) && scrollPos < (item.end - containerSize + threshold)) {
if (i !== props.activeIndex) {
emits('update:activeIndex', i);
}
break;
}
}
};
// 监听activeIndex变化滚动到对应位置
watch(
() => props.activeIndex,
(newVal) => {
scrollToIndex(newVal);
console.log("init",itemPositions.value)
},
{ immediate: true }
);
// 监听itemCount变化重新计算位置
watch(
() => props.itemCount,
async () => {
await initItemPositions();
}
);
// 组件挂载后初始化
onMounted(async () => {
await initItemPositions();
console.log("init",itemPositions.value)
});
// 暴露刷新位置信息的方法给父组件
defineExpose({
refreshPositions: initItemPositions
});
</script>
<style scoped lang="scss">
.scroll-container {
width: 100%;
}
</style>