目录
1 分类
1.1 接口
1.2 窗口限制
1.3 选中状态样式判断
1.4 点击左侧时右侧会到顶点
1.5 源码
2 购物车
2.1 store
2.2 tabBar徽标
2.3 滑动删除
2.4 结算
2.4.1 结算前登录
2.4.2 结算功能
2.5 触发组件事件
2.6 源码
1 分类
分类最上部是搜索区域,与首页的搜索区域相同,这里直接复用
右侧区域的内容 会根据点击不同左侧区域 而变化
左侧区域和右侧区域均可以向下滚动
滚动后切换到别的页面,然后再切换回来,滚动的位置不变(记录位置是scroll-view的默认效果,不用额外进行修改)
1.1 接口
接口中包含三级标题。紫色框子是一级标题,橙色框子是二级标题,绿色框子是三级标题
url是https://api-hmugo-web.itheima.net/api/public/v1/categories 直接发get请求就行了
1.2 窗口限制
分类页面的最外层只有两个部分,一个是搜索框,除了搜索框剩下都属于另外一部分。为了让左侧和右侧都有拖动了效果,所以这里对高度进行了限制
如果取消高度限制,页面中的scroll-view-container就废了,左侧和右侧就会同时进行拖动
限制高度的值根据机型进行调节,wx.getWindowInfo()可以获得当前机型的高度,减50是减去屏幕最下方的tarbar高度。高度在页面初次加载的时候就进行计算
1.3 选中状态样式判断
左侧列表被选中后会有这个红色的部分
每次点击左侧列表会记录left_active_num,讲left_active_num与循环时的index进行比对,如果相同的多给一个active的类名
1.4 点击左侧时右侧会到顶点
每一次点击左侧的时候,右侧的内容是不同的,但实际上元素都是一个。所以点击左侧时,右侧不会默认回到顶点
这里用变量的原因是给0这个常量第二次就懒加载了,只能点击左侧之后每次都赋值一个新的0
1.5 源码
wxml
<!--pages/category/category.wxml-->
<black_horse_search></black_horse_search>
<view class="scroll-view-container" style="height:{{windowHeight}};">
<!-- <view class="scroll-view-container" style="height:200px;"> -->
<!-- 左侧滚动区域 -->
<scroll-view class="left-scroll-view" scroll-y="true">
<view wx:for="{{cateList}}" wx:key="cat_id" class="{{['scroll-view-item',index==left_active_num?'active':'']}}" bindtap="left_scroll_click" data-click_item_index="{{index}}">{{item.cat_name}}</view>
</scroll-view>
<!-- 右侧滚动区域 -->
<!-- 老版本的scroll-top不能给相同的值,也就是你一直给0是不行的 -->
<scroll-view class="right-scroll-view" scroll-y="true" scroll-top="{{right_scrollTop}}">
<view class="cate-lv2" wx:for="{{cateLevel2}}" wx:key="index">
<view class="cate-lv2-title">/ {{item.cat_name}} /</view>
<view class="cate-lv3-list">
<view class="cate-lv3-item" wx:for="{{item.children}}" wx:for-index="index3" wx:for-item="item3" wx:key="index3" bindtap="cate_lv3_item_click" data-cat_name="{{item3.cat_name}}" data-cat_id="{{item3.cat_id}}">
<image src="{{item3.cat_icon}}" mode="" data-cat_name="{{item3.cat_name}}" data-cat_id="{{item3.cat_id}}"/>
<text data-cat_name="{{item3.cat_name}}" data-cat_id="{{item3.cat_id}}" >{{item3.cat_name}}</text>
</view>
</view>
</view>
</scroll-view>
</view>
wxss
/* pages/category/category.wxss */
.scroll-view-container {
display: flex;
}
.scroll-view-container .left-scroll-view {
width: 120px;
}
.scroll-view-container .left-scroll-view .scroll-view-item.active {
position: relative;
background-color: #ffffff;
}
.scroll-view-container .left-scroll-view .scroll-view-item.active::before {
position:absolute;
top:50%;
left:0;
content:' ';
display:block;
width: 3px;
height:30px;
background-color: #C00000;
transform: translateY(-50%);
}
.scroll-view-container .left-scroll-view .scroll-view-item {
background-color: #f7f7f7;
line-height: 60px;
text-align: center;
font-size: 12px;
}
.cate-lv2-title {
font-size:12px;
font-weight:bold;
text-align: center;
padding: 15px 0;
}
.cate-lv3-list {
display:flex;
flex-wrap:wrap
}
.cate-lv3-list .cate-lv3-item {
width:33.33%;
display:flex;
flex-direction:column;
justify-content: center;
align-items: center;
margin-bottom:10px;
}
.cate-lv3-list .cate-lv3-item image{
width:60px;
height:60px;
}
.cate-lv3-list .cate-lv3-item text{
font-size:12px;
}
js
// pages/category/category.js
import {createStoreBindings} from 'mobx-miniprogram-bindings'
import {store} from '../../store/store.js'
const App = getApp()
Page({
cate_lv3_item_click(e) {
wx.navigateTo({
url:'/subpackage_goods_list/goods_list?cid=' + e.target.dataset.cat_id + '&&query=' + e.target.dataset.cat_name
})
},
left_scroll_click(e) {
const click_item_index = e.target.dataset.click_item_index
this.setData({left_active_num:click_item_index})
this.setData({cateLevel2:this.data.cateList[Number(click_item_index)].children})
this.setData({right_scrollTop:0})
},
get_cateList() {
wx.request({
url:App.base_url + '/api/public/v1/categories',
method:'GET',
success:(val) => {
// 获取所有分类
this.setData({cateList:val.data.message})
// 获取二级分类,二级分类的内容在一级分类中有,我们在这里弄好处理,这里给一个初始值,后面在左侧点击事件中还会再改
this.setData({cateLevel2:val.data.message[0].children})
},
fail:() => {
console.log('获取左侧失败')
wx.showToast({
title:'获取左侧失败',
icon:'error',
duration:2000
})
}
})
},
/**
* 页面的初始数据
*/
data: {
windowHeight:0 + 'px',
cateList:[],
cateLevel2:[],
left_active_num:0,
right_scrollTop:0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this,{
store,
actions:['set_cart_tabBar_badge']
})
this.setData({windowHeight:(wx.getWindowInfo().windowHeight-50) + 'px'})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 获取分类列表
this.get_cateList()
// 设置购物车tabBar的徽标
this.set_cart_tabBar_badge()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
2 购物车
购物车中的内容在这个项目中都是放在本地的,在实际开发中一般是放在云端
购物车为空时会显示这个页面
2.1 store
store.js是放全局变量的js文件,创建方法可以参考 18.全局数据共享 mobx-miniprogram与mobx-miniprogram-bindings-CSDN博客
源码
import {
action,
observable
} from 'mobx-miniprogram'
export const store = observable({
// 购物车总数的计算属性
// 计算购物车一共多少件
get cart_shop_total() {
let total = 0
this.cart_list.forEach(goods => total += goods.goods_count)
return total
},
// 计算购物车选中的一共多少钱
get cart_shop_choosed_total_price() {
let total_price = 0
this.cart_list.forEach(goods => {
if (goods.goods_state == true) {
total_price = total_price + goods.goods_count * goods.goods_price
}
})
return total_price.toFixed(2)
},
// 计算购物车选中的一共多少件
get cart_shop_choosed_total_num() {
let total_num = 0
this.cart_list.forEach(goods => {
if (goods.goods_state == true) {
total_num = total_num + goods.goods_count
}
})
return total_num
},
// 计算是否全选
get is_all_checked() {
let all_checked = true
this.cart_list.forEach(goods => {
if (goods.goods_state == false) {
all_checked = false
}
})
return all_checked
},
// 从本地存储中获取购物车商品信息
cart_list: wx.getStorageSync('cart_list') || [],
// 添加购物车方法
add_cart_obj: action(function (goods_obj) {
let findResult = this.cart_list.find((x) => x.goods_id === goods_obj.goods_id)
if (!findResult) {
this.cart_list = [...this.cart_list, goods_obj]
} else {
findResult.goods_count++
}
wx.setStorageSync('cart_list', this.cart_list)
}),
// 设置购物车tabBar的徽标
set_cart_tabBar_badge:action(
function() {
wx.setTabBarBadge({
index: 2,
text: String(this.cart_shop_total),
})
}
),
// 改变购物车商品所有选中状态
updateAllGoodsState:action(
function(all_checked_now_state) {
this.cart_list.forEach(goods => {
goods.goods_state = !all_checked_now_state
})
let new_cart_list = JSON.stringify(this.cart_list)
this.cart_list = JSON.parse(new_cart_list)
wx.setStorageSync('cart_list', this.cart_list)
}
),
// 改变购物车商品选中状态
updateGoodsState:action(
function(goods) {
let findResult = this.cart_list.find(x=>x.goods_id === goods.goods_id)
if (findResult) {
findResult.goods_state = !goods.goods_status
let new_cart_list = JSON.stringify(this.cart_list)
this.cart_list = JSON.parse(new_cart_list)
}
wx.setStorageSync('cart_list', this.cart_list)
}
),
// 购物车商品数量 + 1
add_one_GoodsNum:action(
function(goods) {
let findResult = this.cart_list.find(x=>x.goods_id === goods.goods_id)
if (findResult) {
findResult.goods_count = findResult.goods_count + 1
let new_cart_list = JSON.stringify(this.cart_list)
this.cart_list = JSON.parse(new_cart_list)
}
wx.setStorageSync('cart_list', this.cart_list)
}
),
// 购物车商品数量 - 1
sub_one_GoodsNum:action(
function(goods) {
let findResult = this.cart_list.find(x=>x.goods_id === goods.goods_id)
if (findResult) {
if (findResult.goods_count > 1) {
findResult.goods_count = findResult.goods_count - 1
let new_cart_list = JSON.stringify(this.cart_list)
this.cart_list = JSON.parse(new_cart_list)
}
}
wx.setStorageSync('cart_list', this.cart_list)
}
),
// 删除购物车商品
delete_store_goods:action(
function(goods) {
this.cart_list = this.cart_list.filter(x=>x.goods_id != goods.goods_id)
wx.setStorageSync('cart_list', this.cart_list)
}
),
// 是否是从“购物车”跳转到“我的”界面且需要跳转回“购物车”页面
need_navigate_back:false,
yes_need_navigate_back:action(function() {
this.need_navigate_back = true
}),
cancel_need_navigate_back:action(function() {
this.need_navigate_back = false
})
})
2.2 tabBar徽标
tarBar上的徽标是购物车中商品的数量
用到的是wx.setTabBarBadge()这个api
index是第几个tarbar,按0、1、2这么排,购物车是第二个
text是要写什么东西,类型为字符串,也就是说可以写其他的文字上去
当切换到任意tarbar的时候会更新徽标,在加入购物车的时候会更新徽标,在改变商品数量的时候会更新徽标
使用方法时引入store然后用this调用就行了
2.3 滑动删除
用的是movable-area与movable-view
movable-view必须放在movable-area中,其余就没什么好讲的了,有例子照抄就行了
2.4 结算
2.4.1 结算前登录
这个功能需要“购物车”和“我的”这两个tarbar共同完成,所以需要用到全局数据
这个变量是给"我的"中的一键登录按钮用的,直接点一键登录按钮就不需要跳转,从购物车界面过来再点一键登录按钮就需要跳转
2.4.2 结算功能
目前小程序结算功能是不开放给个人开发者的,如果要开通的话只有一个公司的营业执照也是不行的,需要有实际的开发需求才能用微信支付的接口
2.5 触发组件事件
这些是触发组件的事件,感兴趣可以看一下这个 15-5.自定义组件的通信-CSDN博客
2.6 源码
wxml
<!--pages/cart/cart.wxml-->
<!-- 收货地址区域 -->
<view class="cart-container" wx:if="{{cart_list.length != 0}}">
<view>
<!-- 选择收货地址的盒子 -->
<view class="address-choose-box" wx:if="{{!choosed_address.username}}">
<button type="primary" size="mini" class="btnChooseAddress" bind:tap="go_to_choose_address">请选择收货地址+</button>
</view>
<!-- 渲染收货信息的盒子 -->
<view class="address-info-box" wx:else bind:tap="go_to_choose_address">
<view class="row1">
<view class="row1-left">
<view class="username">收货人:<text>{{choosed_address.username}}</text></view>
</view>
<view class="row1-right">
<view class="phone">电话:<text>{{choosed_address.phone}}</text></view>
<image src="/src/arrowright.png" mode="" style="width:16px;height:16px" />
</view>
</view>
<view class="row2">
<view class="row2-left">收货地址:</view>
<view class="row2-right">{{choosed_address.address_detail}}</view>
</view>
</view>
<!-- 底部的边框线 -->
<image src="/src/cart_border@2x.png" class="address-border"></image>
</view>
<!-- 购物车标题 -->
<view class="cart-title">
<image src="../../src/shop.png" mode="" style="width:18px;height:18px;"/>
<text class="cart-title-text">购物车</text>
</view>
<!-- 购物车内容 -->
<view class="cart_content_view">
<block wx:for="{{cart_list}}" wx:key="index" >
<movable-area data-goods_id="{{item.goods_id}}">
<movable-view direction="horizontal" data-goods_id="{{item.goods_id}}">
<goods_list_item goods_small_logo="{{item.goods_small_logo}}" goods_id="{{item.goods_id}}" goods_price="{{item.goods_price}}" goods_name="{{item.goods_name}}" goods_count="{{item.goods_count}}" showRadio="true" showNumberBox="true" goods_checked="{{item.goods_state}}" bind:radio_change="change_radio_state" bind:add_one="add_one" bind:sub_one="sub_one"></goods_list_item>
<view class="itemDelet" bindtap="delete_goods" data-goods_id="{{item.goods_id}}">删除</view>
</movable-view>
</movable-area>
</block>
</view>
<!-- 底部结算区域 -->
<view class="my-settle-container">
<label class="radio">
<checkbox color="#C00000" checked="{{is_all_checked}}" bind:tap="click_all_checked_button"/><text>全选</text>
</label>
<view class="amount-box">
合计:<text class="amount">¥{{cart_shop_choosed_total_price}}</text>
</view>
<view class="btn-settle" bind:tap="buy_cart">结算({{cart_shop_choosed_total_num}})</view>
</view>
</view>
<!-- 当购物车里什么都没有时出现的东西 -->
<view class="empty-cart" wx:else>
<image src="/src/empty_cart.png" class="empty-img"></image>
<text class="tip-text">空空如也~</text>
</view>
wxss
/* pages/cart/cart.wxss */
/* 购物车标题 */
.cart-title {
height:40px;
display:flex;
align-items:center;
font-size:14px;
padding-left:5px;
border-bottom:1px solid #efefef;
white-space: wrap;
}
.cart-title .cart-title-text {
margin-left:10px;
white-space: wrap;
}
/* 购物车内容的滑动效果 */
movable-area {
display: flex;
flex-direction: row;
width: calc(100% + 120rpx);
justify-content: center;
left: -120rpx;
height: 130px;
z-index:0;
}
movable-view {
display: flex;
flex-direction: row;
width: calc(100% - 120rpx);
z-index: 1001;
left: 120rpx;
}
.itemDelet {
position: absolute;
text-align: center;
right: -125rpx;
line-height: 130px;
height:130px;
background-color: rgb(194,0,2);
margin-top: 0rpx;
margin-right: 6rpx;
width: 100rpx;
text-align: right;
padding-right: 20rpx;
color: #fff;
}
/* 收货地址 */
.address-border {
display: block;
width: 100%;
height: 5px;
}
.address-choose-box {
height: 90px;
display: flex;
align-items: center;
justify-content: center;
}
.address-info-box {
font-size:12px;
height:90px;
display:flex;
flex-direction:column;
justify-content:center;
padding:0 5px;
}
.address-info-box .row1 {
display: flex;
justify-content: space-between;
}
.address-info-box .row1 .row1-right {
display: flex;
align-items: center;
}
.phone {
margin-right: 5px;
}
.address-info-box .row2 {
display: flex;
align-items: center;
margin-top: 10px;
}
.address-info-box .row2 .row2-left {
white-space: nowrap;
}
/* 底部结算区域 */
.my-settle-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 5px;
font-size: 14px;
z-index:999;
}
.my-settle-container .radio {
display:flex;
align-items:center;
}
.my-settle-container .amount {
color:#c00000
}
.my-settle-container .btn-settle {
height: 50px;
min-width: 100px;
background-color: #c00000;
color: white;
line-height: 50px;
text-align: center;
padding: 0 10px;
}
.cart_content_view {
padding-bottom:50px;
}
/* 当购物车什么都没有的时候的区域 */
.empty-cart {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 150px;
}
.empty-cart .empty-img {
width:90px;
height:90px;
}
.empty-cart .tip-text {
font-size:12px;
color:gray;
margin-top:15px;
}
js
// pages/cart/cart.js
import {createStoreBindings} from 'mobx-miniprogram-bindings'
import {store} from '../../store/store.js'
const App = getApp()
Page({
buy_cart() {
if (this.data.cart_shop_choosed_total_num == 0) {
wx.showToast({
title: '请选择要结算的商品',
mask:true,
icon:'none'
})
setTimeout(function() {wx.hideToast()},800)
return
}
if (!this.data.choosed_address.username) {
wx.showToast({
title: '请选择收货地址',
mask:true,
icon:'none'
})
setTimeout(function() {wx.hideToast()},800)
return
}
if (this.data.userinfo == '') {
wx.showToast({
title: '请先登录',
mask:true,
icon:'none'
})
setTimeout(()=>{
wx.hideToast()
wx.switchTab({
url: '/pages/my/my',
success:()=>{
this.yes_need_navigate_back()
}
})
},800)
return
}
},
click_all_checked_button() {
this.updateAllGoodsState(this.data.is_all_checked)
},
go_to_choose_address() {
wx.navigateTo({
url:'/subpackage_choose_address/choose_address'
})
},
change_radio_state(e) {
this.updateGoodsState(e.detail)
},
add_one(e) {
this.add_one_GoodsNum(e.detail)
this.set_cart_tabBar_badge()
},
sub_one(e) {
this.sub_one_GoodsNum(e.detail)
this.set_cart_tabBar_badge()
},
delete_goods(e) {
this.delete_store_goods(e.target.dataset)
this.set_cart_tabBar_badge()
},
/**
* 页面的初始数据
*/
data: {
// address:{'name':'suyu'}
choosed_address:{},
userinfo:'',
from:''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this,{
store,
fields:['cart_list','cart_shop_choosed_total_price','cart_shop_choosed_total_num','is_all_checked'],
actions:['set_cart_tabBar_badge','updateGoodsState','add_one_GoodsNum','sub_one_GoodsNum','delete_store_goods','updateAllGoodsState','yes_need_navigate_back']
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
// 设置购物车tabBar的徽标
this.set_cart_tabBar_badge()
this.setData({choosed_address:wx.getStorageSync('choosed_address') || {}})
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.setData({choosed_address:wx.getStorageSync('choosed_address') || {}})
this.setData({userinfo:wx.getStorageSync('userinfo') || ''})
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})