|
|
<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> |