业务分析:
1 .整个页面分为两种状态:
正常状态:选中商品显示总价,可结算
编辑状态:选中商品增删改查,可删除
页面状态直接影响底部栏的变化
2. 商品,店铺,全选两种状态都可三级联动
3. 编辑状态只有一个店铺能编辑,其他不可编辑
4. 编辑状态对数量操作:增加,减少,输入操作三种改变数量的方式
5. 整体而言,删除商品的情况:正常状态滑动商品单个删除,编辑状态下单个或多个删除及整个店铺删除三种
6.点击删除按钮显示确认对话框,根据用户点击结果发送请求或者直接关闭对话框
7.对话框关闭后从编辑状态切换到正常状态
页面两种状态图示:
准备工作:
1.为所有数据请求接口封装一个入口fetch
export default function fetch({key,data=null,type="post"}){
return new Promise((resolve,reject)=>{
axios[type](URL[key],data).then(res=>{ // URL[key]是封装的完整的地址。 if(res.status === 200){}
if(res.status === 202){}
...
resolve(res)
}).catch(err=>{
reject(err)
})
})
}
2.对同一个场景下所有的操作进行一个封装:
购物车有多种状态,封装cartService对购物车业务状态统一处理
class Cart{
getAllList(){
return _fetch({key:"cartList"}).then(res=>{
return res.data.cartList;
})
}
update(id,number){ // 只要是改变数量的全部放在update return _fetch({key:"cartUpdate",data:{id,number}});
}
remove(id){
return _fetch({key:"cartRemove",data:{id}});
}
removeMore(){
return _fetch({key:"cartRemoveMore",data:{ids}})
}
}
let _cart = new Cart;
export default _cart;
步骤:获取数据进行加工:
获取数据Mock模板:
{
"status": 200,
"message": "",
"cartList|3": [
{
"shopTitle": [ 店铺名
"寻找田野",
"猫咪森林",
"老爹果园"
],
"shopId|+1": 4, 店铺id
"goodsList|1-3": [ 商品列表
{
"id|+1": 6, 商品id
"img": "@image(90x90,@color)", 商品图
"number": 1, 商品数量
"price|50-90.1-2": 1, 商品价格
"sku": [ 商品规格
"全网通,玫瑰金,3+32G",
"全网通,香槟金,3+32G"
],
"title": "VIVO-Y66 全网通/移动版 3+32GB" 商品描述
}
]
}
]
}
从后台获取的数据,先对数据进行加工处理,然后再赋值给data里相应的属性
_cart.getAllList().then(lists=>{
lists.forEach(shop=>{
// checked 响应 页面正常时商品和店铺的icon状态 shop.checked = false ;
// removeChecked 响应 页面编辑时商品和店铺的icon状态 shop.removeCheckd = false;
shop.goodsLIist.forEach(good=>{
good.checked = false;
good.removeChecked = false;
})
})
})
2.页面两种状态通过正在编辑的editingShop的值来判断,editingShop通过点击编辑店铺按钮控制
html:
编辑与完成按钮:
@click="selectEditingShop(shop)"
-----v-show 控制一次只能编辑一个店铺------
v-show="editingShop ? editingShop.shopId === shop.shopId : true"
>
{{editingShop ? "完成":"编辑"}}
我的商品的状态应有所响应:
v-for="(good,goodIndex) in shop.goodsList"
-----editing 让商品样式处理可编辑 -----
:class="{editing : editingShop.editingShopIndex ==== shopIndex : false}"
---------------------------------------
@touchstart($event,good)
@touchend($event,shop,shopIndex,good,goodIndex)
:ref="'goods-'+shopIndex+'-'+goodIndex"
>
底部栏跟着切换
-----isEdit 让底部栏隐藏结算,显示删除----
-----isCheck 当价格大于0,让结算变红,文字变红-----
:class="{isEdit: editingShop, isCheck:totalPrice>0}"
>
// jsselectEditingShop(shop){
this.editingShop = this.editingShop ? null : shop;
}
computed:{
editingShopIndex(){
if(this.editingShop){
return this.cartList.findIndex(shop=>{
return this.editingShop.shopId === shop.shopId
})
}
}
}
3.根据页面状态的三级联动
自下而上:商品 > 店铺 > 全选
1.good:
:class="{checked: editingShop ? good.removeChecked : good.checked}"
---------v-show 只能编辑一个店铺----------
v-show="editingShop ? editingShop.shopId === shop.shopId : true"
@click="selectGood(shop,good)"
>
selectGood(shop,good){
let attr = this.editingShop ? "removeChecked" : "checked";
good[attr] = !good[attr];
// 对店铺 shop[attr] = shop.goodList.every(good=>{
return good[attr];
})
}
2.shop:
:class="{checked: editingShop ? shop.removeChecked : shop.checked}"
---------v-show 只能编辑一个店铺----------
v-show="editingShop ? editingShop.shopId === shop.shopId : true"
@click="selectShop(shop)"
>
selectshop(shop){
let attr = this.editingShop ? "removeChecked" : "checked";
shop[attr] = !shop[attr];
//对商品 shop.goodsList.forEach(good=>{
good[attr] = shop[attr]
})
}
//对全选computd:{
// 正常下的 allChecked:{
get(){
if(this.cartList && this.cartList.length){
return this.cartList.every(shop=>{
return shop.checked;
})
}
},
set(val){}
},
// 编辑下的 allRemoveChecked: {
get(){
if(this.cartList && this.cartList.length){
return this.editingShop.removeChecked;
}
},
set(){}
}
}
3.全选:
:class="{checked:editingShop ? allRemoveChecked : allChecked}"
@click="selectAll"
>全选
selectAll(){
let attr = this.editingShop ? "allRemoveChecked" : "allChecked";
this[attr] = !this[attr];
}
//对商品和店铺computed:{
allChecked:{
get(){...},
set(val){
if (this.cartList && this.cartList.length) {
this.cartList.forEach(shop=>{
shop.checked = val;
shop.goodsList.forEach(good => {
good.checked = val;
})
})
}
}
},
allRemoveChecked:{
get(){...},
set(val){
this.editingShop.removeChecked = val;
this.editingShop.goodsList.forEach(good=>{
good.removeChecked = val;
})
}
}
}
4.此时页面选择情况大致完成,那么我就要动态生成 正常状态选择的列表,或编辑状态的列表,还有总价,是否可以支付
computed:{
selectList(){
if (this.cartList && this.cartList.length) {
let arr = [];
this.cartList.forEach(shop => {
shop.goodsList.forEach(good => {
if(good.checked){
arr.push(good);
}
})
})
return arr;
}
},
removeList(){
if (this.editingShop) {
let arr = [];
this.editingShop.goodsList.forEach(good => {
if(good.removeChecked){
arr.push(good);
}
})
return arr;
}
},
totalPrice(){
let price = 0;
if(this.cartList && this.cartList.length){
this.cartList.forEach(shop=>{
shop.goodsList.forEach(good=>{
if(good.checked) price += good.number*good.price
})
})
}
return price;
},
canpay(){
return (this.totalPrice>0 && !this.editingShop) ? true:false;
}
}
5.更新数量:
:class="disabled:good.number === 1"
@click="updateNumber(good,-1)"
>-
@change="updateNumber(good,good.number)">
@click="updateNumber(good,1)"
>+
updateNumber(good,n){
switch (n){
case 1:
_cart.update(good.id,good.number + 1 ).then(()=>{
// 请求成功再渲染页面 good.number++;
})
break;
case -1:
if(good.number <= 1){return}
_cart.update(good.id,good.number - 1).then(()=>{
good.number--;
})
break;
default:
_cart.update(good.id.n);
}
}
6.删除商品:
1.正常状态点击删除
@click="deleteGood(shop,shopIndex,good,goodIndex,good.id)"
>删除
2.编辑下的删除
:class="{'btn-red':removeList&&removeList.length > 0}"
@click="deleteGood"
>删除
3.滑动删除
...
:ref="'goods-'+shopIndex+'-'+goodIndex"
@touchstart=touchStart($event,good)
@touchend=touchEnd($event,shop,shopIndex,good,goodIndex)
>
js:
deleteGood(shop,shopIndex,good,goodIndex,id){
if(arguments.length){
// 表示只删除一个 this.removeData = {shop,shopIndex,good,goodIndex,id}
}
// 显示对话框 this.isShowPopup = true;
}
确认删除:
confirmRemove(){
if(this.removeData.id){
// 单个 let {shop,shopIndex,good,goodIndex,id} = this.removeData;
_cart.remove(id).then(()=>{
// 成功后渲染 shop.goodsList.splice(goodIndex,1);
// 如果该店铺下无商品 if(!shop.goodsList.length){
this.cartList.splice(shopIndex,1)
}
// 回归正常 this.recoverStatus();
})
}else{
let ids = [];
this.removeList.forEach(good=>{
ids.push(good.id);
})
_cart.removeMore(ids).then(()=>{
if(ids.length === this.editinigShop.goodsList.length){
//全删 this.cartList.splice(this.)
}else{
this.cartList.forEach((shop,shopIndex)=>{
if(shopIndex === this.editingShopIndex){
for(let i = 0;i
if(ids.indexof(shop.goodsLists[i].id) > -1){
shop.goodsList.splice(i,1);
i--;
}
}
}
})
}
this.recoverStatus();
})
}
},
recoverStatus(){
this.removeData = null;
this.editingShop = null;
this.isShowPopup = false;
},
touchStart(e,good){
good.startX = e.changedTouches[0].clientX;
},
touchEnd(e,shop,shopIndex,good,goodIndex){
if(this.editingShop) return; //编辑不可滑动 let endX = e.changedTouches[0].clientX;
let left =0;
if(endX - good.startX > 80){
left = 0;
}else if(endX-good.startX < -80){
left = "-60px";
}
Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`],{left})
}
}
修复bug:
1.编辑时,之前滑动的商品应回归正常状态
recoverSlide(){
this.cartList.forEach((xshop,shopIndex)=>{
xshop.goodsList.forEach((good,goodIndex)=>{
Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`],{left:0});
})
})
}
2.滑动删除后,紧接着的下一个商品会继承滑动的效果,因为v-for采用‘就地复用策略’,复用原有DOM结构,解决:为商品添加唯一识别 ,绑定key,注意不能绑定index
...总结:
1.从后台获取的数据,先对数据进行加工处理,然后再赋值给data里相应的属性。因为是引用,让所有操作都变得对应起来。
2.通过computed 里动态的set 来做响应式处理。这使得data里不需要初始化很多数据,也起到watch的作用。
3.找到突破点,才能让逻辑清晰,有条理。editingShop便是关键。
4.不要闲代码多,乱,先实现需求后优化。。
5.写了好久,我可真蠢,我怀疑我是不是少了哪根筋?????想起来大一计算机基础得了61,我就知道生活不会好过的。。
知识点:
1.$refs 获取子元素/组件列表
ref 是非响应式的,不建议在模板中进行数据绑定,即使用唯一标识绑定
2.v-for 模式使用“就地复用”策略,简单理解就是会复用原有的dom结构,尽量减少dom重排来提高性能 ( 解决方案:还原dom样式 )
3.key 为每个节点提供身份标识,数据改变时会重排,最好绑定唯一标识,如果用index标识可能得不到想要的效果 ( 解决方案:绑定唯一识别key )
4.v-model.number
5.Velocity
6.mockjs
可拓展:
1.分理出失效商品
2.点击结算,判断登录状态,未登录显示对话框跳转登录注册页,登录成功返回购物车页面,已登录状态后生成支付页,支付成功生成查看订单页
Appendix: