1、先看效果
2、以hooks方法处理,方便复制使用,见代码
Good.vue文件
<script setup lang="ts" name="goods">
import {onMounted, ref, nextTick} from "vue";
import useProductScroll from "@/utils/hooks/useProductScroll.ts";
import loading from '@/assets/images/loading.gif'
import categoryHolder from './images/category-holder.png'
import productHolder from './images/product-holder.png'
const productList = ref<any[]>([])
const refSide = ref()
const refMain = ref()
const {mark, setMark, setRefSide, setRefMain} = useProductScroll()
onMounted(() => {
// 模拟productList的数据
const tmpList = Array(15).fill(1).map((v, i) => {
return {
name: `人气热卖${i}`,
pList: Array(5).fill(1).map((v2, i2) => {
return {
name: `名字${i}-${i2}`
}
})
}
})
setTimeout(async () => {
productList.value = tmpList
await nextTick()
setRefSide(refSide.value)
setRefMain(refMain.value)
setMark(tmpList[0].name)
}, 200)
})
</script>
<template>
<div class="product-limit">
<ul class="category-box" ref="refSide">
<li v-for="v in productList" :key="v" :class="{'active': v.name===mark}" @click="() => setMark(v.name)" :mark="v.name">
<div>
<van-image :loading-icon="loading" :src="categoryHolder" lazy-load />
</div>
<span>{{v.name}}</span>
</li>
<li></li>
</ul>
<div class='right-content'>
<div class="search-box">
<van-search shape="round" placeholder="请输入商品关键词" />
</div>
<ul class="com-product-list" ref="refMain">
<li v-for="v in productList" :key="v" :mark="v.name">
<div class="c-category-name">{{v.name}}</div>
<div class="c-detail-box" v-for="v2 in v.pList" :key="v2">
<div class="img-show">
<van-image :loading-icon="loading" :src="productHolder" lazy-load />
</div>
<div class="product-tro">
<p class="c-name">{{v2.name}}</p>
<p class="c-cut-money">
<span>立省11.00元起</span>
</p>
<div class="c-money">
<div class="c-money-detail">
¥ <i>5.60</i>起
<span>¥19.62</span>
</div>
<span class="c-btn-size">选规格</span>
</div>
</div>
</div>
</li>
<li class="product-holder"></li>
</ul>
</div>
</div>
</template>
useProductScroll.ts文件
import {ref} from "vue";
function getDomStyle(dom: HTMLElement, style: any) {
return window.getComputedStyle(dom, null)[style];
}
const catchMainMarkDom: {
[mark: string]: {
dom: HTMLElement,
top: number
}
} = {}
const catchSideMarkDom: {
[mark: string]: {
dom: HTMLElement,
}
} = {}
const catchMainMarkTop: number[] = []
const sideDomView: {
[key: string]: number
} = {}
let timer:any = null
let io:any = null
export default function useProductScroll() {
const mark = ref('')
const refSide = ref()
const refMain = ref()
const setRefSide = (d: HTMLElement) => {
if (getDomStyle(d, 'position') === 'static') {
d.style.position = 'relative'
}
refSide.value = d
// 判断是否在可视区域
io = new IntersectionObserver(entries => {
for (let i=0; i<entries.length; i++) {
const dom = entries[i].target
const m = dom.getAttribute('mark')
sideDomView[m as string] = entries[i].intersectionRatio
}
});
// 缓存
const children: any = d.children
for (let i=0; i<children.length; i++) {
const dom = children[i]
const mName = dom.getAttribute('mark')
if (mName) {
io.observe(dom);
catchSideMarkDom[mName] = {
dom: dom,
}
}
}
}
const setRefMain = (d: HTMLElement) => {
if (getDomStyle(d, 'position') === 'static') {
d.style.position = 'relative'
}
refMain.value = d
// 缓存
const children: any = d.children
for (let i=0; i<children.length; i++) {
const dom = children[i]
const mName = dom.getAttribute('mark')
if (mName) {
catchMainMarkDom[mName] = {
dom: dom,
top: dom.offsetTop
}
catchMainMarkTop.unshift(dom.offsetTop)
}
}
// 绑定
refMain.value.addEventListener('scroll', bindMainScroll)
}
const setMark = (str: string) => {
mark.value = str
const dom = catchMainMarkDom[str]['dom']
dom.scrollIntoView({
behavior: "smooth"
});
}
const bindMainScroll = (e: any) => {
clearTimeout(timer)
const scrollTop = e.target.scrollTop
timer = setTimeout(() => {
// 判断top值和scroll比较,获取最近的top值,获取新的mark值
let newTop = 0
for (let i=0; i<catchMainMarkTop.length; i++) {
if (scrollTop >= catchMainMarkTop[i]) {
newTop = catchMainMarkTop[i]
break;
}
}
let markName = ''
// 通过newTop值,获取新的mark名称
for (let mName in catchMainMarkDom) {
if (catchMainMarkDom[mName]['top'] === newTop) {
markName = mName
break
}
}
if (mark.value === markName) return;
mark.value = markName
const isView = sideDomView[markName] > 0
if (!isView) {
catchSideMarkDom[markName]['dom'].scrollIntoView({
behavior: "smooth"
});
}
}, 200)
}
const unbind = () => {
refMain.value.removeEventListener('scroll', bindMainScroll)
io?.disconnect()
io = null
}
return {
mark,
setMark,
setRefSide,
setRefMain,
unbind
}
}
3、使用,hooks抛出了5个方法,作用分别是:
mark:标示字符,用于判断分类
setMark:当分类点击时,传入mark值
setRefSide:传入dom元素,分类的scroll元素
setRefMain:传入dom元素,商品的scroll元素
unbind:组件卸载时调用
4、使用规则
a、用原生滚动
b、需要在页面渲染后使用
c、依次调用setRefSide,setRefMain,setMark
d、在分类列表和产品列表,scroll元素的子元素,需要绑定mark标示,用于匹配