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

1 week ago
<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"
4 days ago
@touchmove.prevent="handleTouchMove"
style="touch-action: none;"
1 week ago
>
<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); // 滚动容器信息
4 days ago
const handleTouchMove = (e)=> {
e.preventDefault(); // 强制阻止默认滚动行为
return false; // 增强阻止效果
}
1 week ago
// 获取元素信息的工具函数
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 {
4 days ago
width: 100%;
1 week ago
}
</style>