布局:
上、可供选择的栏目、tabs
下:左类别 + 友相对应的菜品 + 悬浮购物车(position:fixed)
左:可滑动的类别选择、scroll-view
右:可滑动的对应菜品、scroll-view
功能:
加入购物车特效
栏目切换
JS计算价格
实际成品(未完善):
动手:
栏目切换:
<view class="tabs-box">
<block wx:for="{{ tabs }}" wx:key="unique">
<view class="tabs-item {{currentTabsIndex==index?'selected':''}}" bindtap="onTabsItemTap" data-index="{{index}}">
{{item}}
view>
block>
view>
.tabs-box{
height: 70rpx;
display: flex;
justify-content: space-between;
}
.tabs-item{
width: 50%;
color: #C7C7CB;
font-size: 28rpx;
align-items: center;
justify-content: center;
display: flex;
border-bottom: 1rpx solid #D0D0D7;
}
.tabs-item.selected{
color: #ff7440;
border-bottom: 2px solid #ff7440;
}
//tab切换
onTabsItemTap: function (event) {
let that = this;
//let index = event.detail.dataset.value?记不太清了、可以自己console一下
let index = shop.getDataSet(event, 'index');
this.setData({
currentTabsIndex: index
})
//载入评论数据
if (this.data.currentTabsIndex == 1) {
comments.loadComments(this.data.id, (res) => {
that.setData({
'commentsInfo': res,
});
(res.code == 1) && that.calcComments(res.result);
});
}
},
左边菜单栏:
差不多同理、设置好区域切换时显示不同的区域就行了
右边对应菜品:
此处用了个贝塞尔曲线、完成飞入购物车的特效
<scroll-view scroll-y="{{true}}" style="height:900rpx;" bindscroll="scroll" scroll-into-view="{{toView}}">
<view class="shopDishes">
<view class="shopDishes-box" wx:for="{{ dishes }}" wx:key="unique">
<view data-src="{{ item.img_url }}" bindtap="aloneImgPreview">
<image class="shopDishes-img" src="{{ item.img_url ? item.img_url : '../../images/icon/blank.png' }}" mode="aspectFill">image>
view>
<view class="shopDishes-info">
<text class="shopDishes-title">{{item.name}}text>
<text class="shopDishes-price">¥{{item.price}}text>
<text class="shopDishes-desc">{{item.description}}text>
view>
<view class="good_box" hidden="{{hide_good_box}}" style="left: {{bus_x}}px; top: {{bus_y}}px;">view>
<view class="add" catchtap="touchOnGoods" forbid="{{true}}">
<view class="calcPic">
<image src="../../images/icon/plus.png" bindtap="dishesCalc" data-id="{{ item.id }}" data-char="add">image>
view>
view>
<view class="calcPic subtract">
<image wx:if="{{ item.counts>0 }}" src="../../images/icon/subtract.png" bindtap="dishesCalc" data-id="{{ item.id }}" data-char="subtract">image>
<text wx:if="{{ item.counts>0 }}">{{ item.counts }}text>
view>
view>
<view class="blankImg" hidden="{{ shopInfo.food[0].id }}">
<image src="../../images/icon/blank.png">image>
view>
view>
scroll-view>
view>
这里想用冒泡事件来完成点击加入购物车同时触发贝塞尔曲线的点击事件、然后小程序好像不太支持pointer-event事件、然后再父函数里做处理成功完成了
.shopDishes{
background-color: #eee;
}
.shopDishes-box{
display: flex;
background-color: #fff;
margin-bottom: 10rpx;
padding: 10rpx;
}
.shopDishes-info{
margin: 0 10rpx 10rpx 10rpx;
display: flex;
line-height: 2.0;
flex-direction: column;
}
.shopDishes-title{
font-weight: 900;
font-size: 30rpx;
}
.shopDishes-price{
color: red;
}
.shopDishes-desc{
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
align-content: center;
font-size: 22rpx;
}
.shopDishes-img{
height: 240rpx;
width: 240rpx;
margin: 10rpx;
}
.add{
/* pointer-events: none; */
}
.calcPic{
/* pointer-events: visiblePainted; */
display: flex;
position: absolute;
right: 15rpx;
align-items: center;
}
.calcPic image{
width: 35rpx;
height: 35rpx;
margin: 15rpx;
}
.subtract{
right: 75rpx;
}
//scroll
scroll:function(event){
},
//外卖计算
dishesCalc:function(event){
let dishesId = shop.getDataSet(event, 'id');
let char = shop.getDataSet(event, 'char');
let index = this._getIndexById(dishesId);
let tempObj = {};
for (let key in this.data.shopInfo.food) {
let dishes = this.data.shopInfo.food[key]
if (dishesId == dishes.id) {
tempObj = this.data.shopInfo.food[key];
break;
}
}
//添加、增加
if (char == 'add'){
if(this.data.shopInfo.food[index].counts > 0){
this.data.shopInfo.food[index].counts += 1;
// this.touchOnGoods();
}
else{
this.data.shopInfo.food[index].counts = 1;
}
shopcar.add(tempObj, 1, this.data.shopInfo.name); //数量增加
}
else {
shopcar.subtractCounts(dishesId) //数量减少
this.data.shopInfo.food[index].counts -= 1;
}
//渲染
this.setData({
dishes: this.data.shopInfo.food
})
},
贝塞尔曲线实现(计算出点、然后根据点画线):
//飞入购物车特效、贝塞尔曲线
flytocar: function(){
//获取屏幕[宽、高]
var that = this;
wx.getSystemInfo({
success: function (res) {
that.globalData.ww = res.windowWidth;
that.globalData.hh = res.windowHeight;
}
})
},
/**
* @param sx 起始点x坐标
* @param sy 起始点y坐标
* @param cx 控制点x坐标
* @param cy 控制点y坐标
* @param ex 结束点x坐标
* @param ey 结束点y坐标
* @param part 将起始点到控制点的线段分成的份数,数值越高,计算出的曲线越精确
* @return 贝塞尔曲线坐标
*/
bezier: function (points, part) {
let sx = points[0]['x'];
let sy = points[0]['y'];
let cx = points[1]['x'];
let cy = points[1]['y'];
let ex = points[2]['x'];
let ey = points[2]['y'];
var bezier_points = [];
// 起始点到控制点的x和y每次的增量
var changeX1 = (cx - sx) / part;
var changeY1 = (cy - sy) / part;
// 控制点到结束点的x和y每次的增量
var changeX2 = (ex - cx) / part;
var changeY2 = (ey - cy) / part;
//循环计算
for (var i = 0; i <= part; i++) {
// 计算两个动点的坐标
var qx1 = sx + changeX1 * i;
var qy1 = sy + changeY1 * i;
var qx2 = cx + changeX2 * i;
var qy2 = cy + changeY2 * i;
// 计算得到此时的一个贝塞尔曲线上的点
var lastX = qx1 + (qx2 - qx1) * i / part;
var lastY = qy1 + (qy2 - qy1) * i / part;
// 保存点坐标
var point = {};
point['x'] = lastX;
point['y'] = lastY;
bezier_points.push(point);
}
//console.log(bezier_points)
return {
'bezier_points': bezier_points
};
}
shop.js
import { Shopcar } from '../shopcar/shopcar-model.js';
var shopcar = new Shopcar();
Page({
data:{
currentTabsIndex: 0, //Tabs选项卡当前index
dishesTabsIndex: 0, //dishesTabs
hide_good_box: true, //贝塞尔曲线
},
onLoad:function(options){
this.busPos = {}; //购物车坐标
this.busPos['x'] = app.globalData.ww / 4; //落地点坐标
this.busPos['y'] = app.globalData.hh - 10;
},
//飞入购物车特效
touchOnGoods: function (e) {
//如果非冒泡调用、不执行
if(! e.target.dataset.char){
return 0;
}
// 如果good_box正在运动
if (!this.data.hide_good_box) return;
this.finger = {};
var topPoint = {};
this.finger['x'] = e.touches["0"].clientX;
this.finger['y'] = e.touches["0"].clientY;
if (this.finger['y'] < this.busPos['y']) {
topPoint['y'] = this.finger['y'] - 150;
} else {
topPoint['y'] = this.busPos['y'] - 150;
}
topPoint['x'] = Math.abs(this.finger['x'] - this.busPos['x']) / 2;
if (this.finger['x'] > this.busPos['x']) {
topPoint['x'] = (this.finger['x'] - this.busPos['x']) / 2 + this.busPos['x'];
} else {
topPoint['x'] = (this.busPos['x'] - this.finger['x']) / 2 + this.finger['x'];
}
this.linePos = app.bezier([this.finger, topPoint, this.busPos], 20);
this.startAnimation();
},
startAnimation: function () {
var index = 0,
that = this,
bezier_points = that.linePos['bezier_points'],
len = bezier_points.length - 1;
this.setData({
hide_good_box: false,
bus_x: that.finger['x'],
bus_y: that.finger['y']
})
this.timer = setInterval(function () {
index++;
that.setData({
bus_x: bezier_points[index]['x'],
bus_y: bezier_points[index]['y']
})
if (index >= len) {
clearInterval(that.timer);
that.setData({
hide_good_box: true,
})
}
}, 15);
},
计算部分比较冗杂、不好梳理、直接贴代码了、有用的取、抱歉!
shopcar-model.js
import { Base } from '../../utils/base.js';
export class Shopcar extends Base {
constructor() {
super();
this._storageKeyName = ''; //‘’、不同的商家下、对应不同的缓存
};
//添加、有则+1、无则新建
add(item, counts, key){
this._storageKeyName = key;
let carData = this.getCarDataFromLocal();
if (!carData) {
carData = [];
}
let isHasInfo = this._isHasThatOne(item.id, carData);
if(isHasInfo.index == -1){
item.counts = counts;
item.selectedStatus = true;
carData.push(item);
}
else{
carData[isHasInfo.index].counts += counts;
}
this.execSetStorageSync(carData);
}
//获取本地数据、flag获取勾选的商品
getCarDataFromLocal(key, flag){
if(key){
this._storageKeyName = key;
}
let res = wx.getStorageSync(this._storageKeyName);
if(!res){
res = [];
}
if(flag){
let newRes = [];
for(let i=0, len=res.length; i<len; i++){
if(res[i].selectedStatus){
newRes.push(res[i]);
}
}
res = newRes;
}
return res;
}
//判断是否存在
_isHasThatOne(id, arr){
let item, result = { index: -1 };
for(let i=0, len=arr.length; i<len; i++){
item = arr[i];
if(item.id == id){
result = {
index: i,
data: item
};
break;
}
}
return result;
}
//商品总数、选中或未选中
getCarTotalCounts(flag){
let data = this.getCarDataFromLocal
let counts = 0;
for(let i=0, len=data.length; i<len ;i++){
//选中
if(flag){
if (data[i].selectedStatus){
counts += data[i].counts;
}
}
//未选中
else{
counts += data[i].counts;
}
}
return counts;
}
//修改商品数量、数量为0时删除
_changeCounts(id, counts){
let carData = this.getCarDataFromLocal(), hasInfo = this._isHasThatOne(id, carData);
if(hasInfo.index != -1){
if(hasInfo.data.counts > 0){
carData[hasInfo.index].counts += counts;
if ( carData[hasInfo.index].counts==0 ){
carData.splice(hasInfo.index, 1);
}
}
}
this.execSetStorageSync(carData);
}
//+
addCounts(id){
this._changeCounts(id, 1);
}
//-
subtractCounts(id){
this._changeCounts(id, -1);
}
//删除
deleteDishes(ids){
if(!(ids instanceof Array)){ //转为数组
ids = [ids];
}
let carData = this.getCarDataFromLocal();
for(let i=0; i<ids.length; i++){
let hasInfo = this._isHasThatOne(ids[i], carData);
if(hasInfo.index != -1){
carData.splice(hasInfo.index, 1);
}
}
this.execSetStorageSync(carData);
}
/*公开方法,缓存*/
execSetStorageSync(data) {
wx.setStorageSync(this._storageKeyName, data);
}
//除去缓存
removeStorage(key){
this._storageKeyName = key;
let data = this.getCarDataFromLocal(this._storageKeyName);
if( data.length>0 ){
wx.removeStorageSync(this._storageKeyName);
}
}
}