微信小程序-实现类似美团外卖店铺页面滑动左右联动效果
2020-12-27 本文已影响0人
侬姝沁儿
代码案例:微信小程序-实现类似美团外卖店铺页面滑动左右联动效果(代码案例)
思考
首先看看简易效果图:
简易效果图
通过scroll-view来实现,要求是点击menu,滚动到锚点;滚动到锚点,激活相应的menu。
思考:
1.思考页面布局
2.思考可能使用到的技术点
技术点:
1.点击滚动到锚点位置:可以通过 scroll-view 的 scroll-into-view="" 属性来实现。scroll-view
2.滚动到锚点,激活相应的menu:滚动list的时候,记录 scroll-view 的 scrollTop;获取相关wxml元素的高度,根据高度算出来每个锚点的scrollTop;menu根据 scrollTop 是否滚动到相应锚点,去确定决定是否激活。
3.吸顶效果采用:position: sticky;。
4.页面滚动到后面时,左侧相应的menu可能会被隐藏,因此我们需要在左侧menu变为下面时,把左侧的向上滚动。
关于滚动与menu的上层结构设计大概如下:
<view class="sidebar-scroll-view custom-class">
<!-- left -->
<scroll-view class="scroller-left scroller-left-class" id="scroller-left" scroll-y="{{ true }}" scroll-top="{{ scrollLeftTop }}">
</scroll-view>
<!-- right -->
<scroll-view class="scroller-right scroller-right-class" id="scroller-right" scroll-y="{{ true }}" scroll-with-animation="{{ true }}" scroll-into-view="{{ scrollLocationId }}" bindscroll="onScrollRightEvent">
</scroll-view>
</view>
关于文件目录如下:
文件目录
代码:
index.html
<!--pages/index/index.wxml-->
<view class="index">
<!-- header -->
<view class="index-header">
<view class="search">
<input class="search-input" type="text" placeholder="搜索 商品名称" />
</view>
</view>
<!-- body -->
<view class="index-body">
<sidebar-scroll-view wx:if="{{ list.length !== 0 }}" custom-class="sidebar-scroll-view" list="{{ list }}"></sidebar-scroll-view>
</view>
<!-- footer -->
<view class="index-footer">
<!-- goods-action -->
<view class="goods-action">
<view class="goods-action-icon">
<image class="goods-action-icon__icon" src="https://wximg.bdsimg.com/thxx_v2/wxapp/images/icon-home-gray.png"></image>
首页
</view>
<view class="goods-action-icon">
<image class="goods-action-icon__icon" src="https://wximg.bdsimg.com/thxx_v2/wxapp/images/icon-home-gray.png"></image>
购物车
</view>
<view class="goods-action-btns">
<button class="goods-action-button goods-action-button--first goods-action-button--both" style="background:linear-gradient(to right, #F6C644, #FF8600);" loading="{{ addcartLoading }}">
<text class="goods-action-button__text">加入购物车</text>
</button>
<button class="goods-action-button goods-action-button--first goods-action-button--both" style="background:linear-gradient(to right, #FF6300, #FF5030);" loading="{{ addcartLoading }}">
<text class="goods-action-button__text">立即购买</text>
</button>
</view>
</view>
</view>
</view>
index.wxss
/* pages/index/index.wxss */
.index {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
/* header */
.index-header {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx;
height: 112rpx;
box-sizing: border-box;
}
.search {
position: relative;
flex: 1;
padding: 12rpx 80rpx;
border-radius: 32rpx;
background-color: #F5F5F5;
}
.search-input {
line-height: 40rpx;
font-size: 26rpx;
color: #CCCCCC;
font-weight: 400;
}
/* body */
.index-body {
flex: 1;
display: flex;
flex-direction: row;
height: calc(100vh - 112rpx - 240rpx);
width: 100vw;
}
/* footer */
.index-footer {
height: 100rpx;
border-top: 1px solid #F5F5F5;
}
/* goods-action */
.goods-action {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
padding-bottom: env(safe-area-inset-bottom);
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
box-sizing: content-box;
height: 100rpx;
background-color: #fff;
border-top: 2rpx solid #ececec;
}
.goods-action-icon {
position: relative;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
-webkit-box-pack: center;
justify-content: center;
min-width: 118rpx;
height: 100%;
color: #828382;
font-size: 24rpx;
line-height: 1;
text-align: center;
background-color: #fff;
cursor: pointer;
}
.goods-action-icon__icon {
display: block;
width: 44rpx;
height: 44rpx;
font-size: 44rpx;
line-height: 44rpx;
margin: 0 auto 10rpx;
color: #828382;
}
.goods-action-icon__icon-dot {
position: absolute;
top: 0;
right: 0;
box-sizing: border-box;
min-width: 32rpx;
padding: 0 6rpx;
color: #fff;
font-weight: 500;
font-size: 24rpx;
line-height: 28rpx;
text-align: center;
background-color: #ee0a24;
border: 1px solid #fff;
border-radius: 28rpx;
}
.goods-action-btns {
-webkit-box-flex: 1;
-webkit-flex: 1;
flex: 1;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
justify-content: center;
}
.goods-action-button {
-webkit-box-flex: 1;
-webkit-flex: 1;
flex: 1;
height: 68rpx;
line-height: 68rpx;
font-weight: 500;
font-size: 28rpx;
border: none;
box-sizing: border-box;
width: 100% !important;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border-radius: 68rpx;
background-color: ;
}
.goods-action-button--both {
min-width: 195rpx !important;
}
.goods-action-button--one {
min-width: 385rpx !important;
margin: 0 !important;
}
.goods-action-button .goods-action-button__text {
font-size: 28rpx;
font-weight: 400;
color: #fff;
}
.goods-action-button--last {
margin-right: 5px;
border-top-right-radius: 999px;
border-bottom-right-radius: 999px;
}
.goods-action-button--first.van-button {
margin-left: 5px;
border-top-left-radius: 999px;
border-bottom-left-radius: 999px;
}
index.js
// pages/index/index.js
import dataJson from '../../data/data'
Page({
/**
* 页面的初始数据
*/
data: {
list: [],
submitLoading: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
console.log(dataJson.result)
setTimeout(() => {
this.setData({
list: dataJson.result
})
}, 1500)
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
}
})
index.json
{
"usingComponents": {
"sidebar-scroll-view": "./components/sidebar-scroll-view/sidebar-scroll-view"
}
}
components/sidebar-scroll-view/sidebar-scroll-view.wxml
<!--pages/index/components/sidebar-scroll-view/sidebar-scroll-view.wxml-->
<view class="sidebar-scroll-view custom-class">
<!-- left: bindscroll="onScrollLeftEvent" -->
<scroll-view class="scroller-left scroller-left-class" id="scroller-left" scroll-y="{{ true }}" scroll-top="{{ scrollLeftTop }}">
<!-- sidebar-menu -->
<view class="sidebar-menu">
<view
class="sidebar-item {{ scrollRightTop >= scrollTopList[index] && scrollRightTop < scrollTopList[index + 1] ? 'sidebar-item--selected' : '' }}"
hover-class="sidebar-item--hover"
hover-stay-time="70"
wx:for="{{ list }}"
wx:key="index"
data-index="{{ index }}"
bind:tap="handleSidebarChange"
>
<view>{{ item.categoryName }}</view>
<view style="color:;font-size:;font-weight:400;">{{ item.confirmedNum }}/{{ item.skuNum }}</view>
</view>
</view>
</scroll-view>
<!-- right -->
<scroll-view class="scroller-right scroller-right-class" id="scroller-right" scroll-y="{{ true }}" scroll-with-animation="{{ true }}" scroll-into-view="{{ scrollLocationId }}" bindscroll="onScrollRightEvent">
<!-- product -->
<view class="product-body">
<!-- chunk -->
<view class="product-body__chunk" wx:for="{{ list }}" wx:key="index">
<!-- chunk-title -->
<view class="product-chunk__title">{{ item.categoryName }}</view>
<!-- 锚点定位: 必须这样子 -->
<view class="anchor-point" id="anchor-point-title-{{ index }}"></view>
<!-- product-list -->
<view class="product-list" id="product-chunk-list-{{ index }}">
<view class="product-list__item" wx:for="{{ item.productList }}" wx:for-index="idx" wx:for-item="goodsItem" wx:key="idx">
<image
class="product-list__item-pic"
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpuui.qpic.cn%2Fvshpic%2F0%2FmgJtAXymTWfJSZbCFzCX8ROr6wHQQoUWGvQ28eO-8qcA42WP_0%2F0.jpg&refer=http%3A%2F%2Fpuui.qpic.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1611556396&t=b6f7bfcc5c0f80bc0680be5183dd734e"
>
</image>
<view class="product-list__item-title">{{ goodsItem.productName }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
components/sidebar-scroll-view/sidebar-scroll-view.wxss
/* pages/index/components/sidebar-scroll-view/sidebar-scroll-view.wxss */
.sidebar-scroll-view {
display: flex;
flex-direction: row;
width: 100vw;
height: 100%;
}
/* left */
.scroller-left {
width: 168rpx;
padding: 0;
margin: 0;
}
/* right */
.scroller-right {
display: flex;
width: calc(100vw - 168rpx);
flex: 1;
padding: 0;
margin: 0;
overflow: hidden;
}
/* sidebar-menu */
.sidebar-menu {
width: 168rpx;
}
.sidebar-item {
display: block;
box-sizing: border-box;
overflow: hidden;
border-left: 3px solid transparent;
user-select: none;
padding: 28rpx 24rpx;
font-size: 28rpx;
line-height: 38rpx;
color: #666;
background-color: #F5F5F5;
}
.sidebar-item--hover:not(.sidebar-item--disabled) {
background-color: #f2f3f5;
}
.sidebar-item--selected {
color: #333333;
font-weight: 600;
}
.sidebar-item--selected,
.sidebar-item--selected.sidebar-item--hover {
background-color: #fff;
}
.sidebar-item--disabled {
color: #c8c9cc;
}
/* product */
.product-body {
flex: 1;
width: 100%;
}
/* chunk */
.product-body__chunk {
}
/* chunk-title */
.product-chunk__title {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 100;
line-height: 52rpx;
font-size: 26rpx;
color: #333333;
font-weight: 400;
padding: 0 14rpx;
background-color: #FFFFFF;
}
/* product-list */
.product-list {}
.product-list__item {
position: relative;
height: 196rpx;
width: 100%;
box-sizing: border-box;
padding: 16rpx;
padding-left: 192rpx;
}
.product-list__item-pic {
position: absolute;
top: 16rpx;
left: 16rpx;
width: 160rpx;
height: 160rpx;
border-radius: 15rpx;
background-color: #D8D8D8;
}
.product-list__item-title {
line-height: 32rpx;
font-size: 28rpx;
color: #333333;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.anchor-point {
width: 100%;
height: 0;
margin: 0;
padding: 0;
}
components/sidebar-scroll-view/sidebar-scroll-view.json
{
"component": true,
"usingComponents": {}
}
components/sidebar-scroll-view/sidebar-scroll-view.js
// pages/index/components/sidebar-scroll-view/sidebar-scroll-view.js
import debounce from "../../../../utils/debounce.js"
let oldActiveKey = 0
Component({
/**
* 组件接受的外部样式类
*/
externalClasses: ['custom-class'],
/**
* 组件的属性列表
*/
properties: {
list: {
type: Array,
value: []
},
},
/**
* 组件的初始数据
*/
data: {
scrollLeftHeight: 0, // scroller-left 的高
activeKey: 0, // sidebar activeKey
scrollLeftTop: 0, // 分类 scrollTop
// 锚点定位 与 滚动定位相应 sidebar-menu
scrollLocationId: null, // 滚动定位Id
scrollRightTop: 0, // 商品 scrollTop
productChunkRects: [], // class product-body__chunk 的 rects
scrollTopList: [], // class product-body__chunk 的每个开始的 scrollTop
},
lifetimes: {
// 在组件实例进入页面节点树时执行
attached: function () {},
// 在组件在视图层布局完成后执行
ready: function () {
this.getProductChunkRectsAndScrollTop() // 获取产品块的rects
// 获取 scroller-left 的高
wx.createSelectorQuery().in(this).select('#scroller-left').boundingClientRect((rect) => {
console.log('scroller-left', rect)
this.setData({
scrollLeftHeight: rect.height
})
}).exec()
}
},
/**
* 组件的方法列表
*/
methods: {
// scroll --------------------------------------------------------
// 商品 scroll-view 滚动事件
onScrollRightEvent(event) {
// console.log('onScrollEvent', event, event.detail)
const {
scrollTop
} = event.detail
this.setData({
scrollRightTop: scrollTop
})
debounce(500, () => {
this.computeCurrSidebarActiveByScrollTop(scrollTop)
})()
},
// scroll --------------------------------------------------------
// 锚点定位 --------------------------------------------------------
// 切换 sidebar
handleSidebarChange(event) {
// console.log('handleSidebarChange', event)
let activeKey = event.currentTarget.dataset.index
this.setData({
activeKey
})
// 锚点定位
this.gotoAnchorPointLocation(`anchor-point-title-${activeKey}`)
},
// 锚点定位与scrollTop
gotoAnchorPointLocation(scrollLocationId) {
// console.log('gotoAnchorPointLocation', scrollLocationId)
this.setData({
scrollLocationId
})
},
// 锚点定位 --------------------------------------------------------
// 滚动定位相应 sidebar-menu ----------------------------------------
// 获取产品块的rects
getProductChunkRectsAndScrollTop() {
wx.showLoading({
title: '加载中...',
mask: true
})
wx.createSelectorQuery().in(this).selectAll('.product-body__chunk').boundingClientRect((rects) => {
wx.hideLoading()
let scrollTopList = []
let acc = rects.map(item => item.height).reduce(function (accumulator, currentValue) {
scrollTopList.push(accumulator)
return accumulator + currentValue
}, 0)
scrollTopList.push(acc)
console.log('scrollTopList', scrollTopList)
// console.log('getProductChunkRectsAndScrollTop', rects)
this.setData({
productChunkRects: rects,
scrollTopList
})
}).exec()
},
// 通过当前滚动位置计算当前 Sidebar 的 Active
computeCurrSidebarActiveByScrollTop(scrollTop) {
if (this.data.productChunkRects.length !== 0) {
let currScrollTopList = this.data.scrollTopList.filter(item => item <= scrollTop)
let activeKey = currScrollTopList.length - 1 >= 0 ? currScrollTopList.length - 1 : 0
// console.log(currScrollTopList, scrollTop, activeKey, oldActiveKey)
// 不直接复制通过 this.setData({ activeKey }),为了加快渲染速度
if (oldActiveKey !== activeKey) {
oldActiveKey = activeKey
this.setData({
activeKey
}, () => {
// 动态显示 Sidebar 的 activeKey 处在一个合理的位置
let scrollLeftTop = Math.floor((activeKey + 1) * 70 / this.data.scrollLeftHeight) * this.data.scrollLeftHeight - 70
// console.log(scrollLeftTop, this.data.scrollLeftHeight)
this.setData({
scrollLeftTop: scrollLeftTop > 0 ? scrollLeftTop : 0
})
})
}
// console.log('----', currScrollTopList, activeKey, oldActiveKey)
} else {
this.getProductChunkRectsAndScrollTop()
}
},
// 锚点定位 ----------------------------------------
}
})