Vue3+TS+移动端-购物车实现详细步骤+项目优化
vue3购物车详情思路,再加项目优化
分析得到:因为购物车,在几个页面都是需要进行数据互通,主页面,商品页面,详情页面,三个不同的组件,要实现一个页面数据变化,另外页面数据跟着变化,那必须要使用vue中vuex,因为是vue3也可以使用pinia
vuex官网:https://vuex.vuejs.org/zh/
pinia官网:https://pinia.web3doc.top/
这次购物车就选用vuex:
第一步:需要配置自动导入
在vite.config.ts中配置
第二步: 修改store-->index.ts 要实现模块化开发
注意:建立好模块之后,index.ts需要引入
第三步: 配置完成之后,需要重启项目,一定要重新启动
第四步:还需要下载依赖,配置vuex数据持久化存储
vuex的特点是: 全局变量,
他有一个缺点: 如果页面刷新,那我们做的全局变量会初始化
怎么解决这个问题:
用的底层原理就是将 vuex 的全局变量放到 localstorage
localstorage,生命周期,您不删除的话,他不会删除
sessionstorage 生命周期,浏览器关闭了,就会自动删除
官方: 持久化插件
vuex刷新之后他的数据初始化的问题.
用的是持久化插件
下载vuex数据持久化的插件
npm i vuex-persistedstate@4.1.0 --save
或者 :
cnpm i vuex-persistedstate@4.1.0 --save
第五步: 配置插件,在index.ts文件中配置,数据持久存储
完整代码段:
整个代码
import { createStore } from 'vuex'
import cart from "./modules/cart"
import createPersistedState from "vuex-persistedstate";
export default createStore({
modules: {
cart
},
plugins: [createPersistedState({
//指定数据被存储在哪里,当值为window.localStorage则表示存储在localStorage中,
// 当值为window.sessionStorage则表示存储在sessionStorage中
storage: window.localStorage,
key: 'sellcard',
paths: ["cart"]
})],
})
最后:项目准备工作已经完成了,所有的配置已经完成,接下来就是写代码,实现购物车的业务逻辑
购物车业务逻辑部分:
第一步:在全局变量中定义变量
首先要确定什么类型,是数组,对象,还是字符串,因为是购物车,有很多商品,商品包括名字,价格,数量等.肯定是数组比较合适
state: {
// 放全局变量的
// 定义一个数组存入 你往购物车添加的所有商品
cartList: []
},
第二步: 添加购物车的点击事件
添加购物车,需要进行传参,把整个数据传过去
// vue2中store 使用this.$store 因为他是全局挂载
// vue3中store 是按需导入的 不能使用this.$store
// vue3中 let store = new useStore()
// vue3语法糖中 new 不要
// let store = useStore()
let store = useStore();
//第一个方法是把商品添加到购物车
let addToCart = (food: any) => {
//这个方法要操作 全局变量
console.log(food);
store.commit("setData", food);
};
第三步: 在store的mutations里面设置一个方法
设置这个方法主要是修改改变state中的数据
mutations: {
//同步的修改 state的值的方法
setData(state: any, foodobj: any) {
// foodobj
// 没有点餐的数量 要呀
// 你在点击添加商品的时候.相当于把商品添加进来,然后
// 数量默认为1
state.cartList.push({ count: 1, ...foodobj })
}
},
第四步: 点击添加购物车案例
小bug出现:点击购物车,跳转到详情页面去了,主要是事件冒泡,需要阻止事件冒泡
第五步: 实时的更新 商品的状态
主要是解决:有数量的时候显示数据,如果数量为0,显示"添加购物车"
v-if和v-else进行判断
难点:关键是怎么判断,到底什么时候v-if,什么时候v-else呢
问题:
//他不是点击触发出的点击事件
//详情页面也有一个添加购物车
//我点击详情页面的添加购物车,
//商品的按钮就会消失
//因为我已经添加了购物车
解决思路:vue中可以用监听的方式来实时掌握数据源的变化,当数据源发生了变化,就触发这个v-if或者v-else;而且它们返回的true或者false
1.可以用computed实时监听数据源的变化,但是需要传参过去
注意:但是我们需要进行传参,然而计算属性不能传值,用的办法就是返回一个函数,因为函数是可以进行接受传参的,可以解决这个不能传参的问题
let isHaveFood = computed(() => {
let fn = (id: any) => {
//因为v-if最终的结果为true /false
// 我这里的方法会返回一个true /false
// 根据数据源的变化而变化
// 这个数据源在 全局变量中
// 实时的监听全局变量中的cartList的变化
//store.getters
return store.getters.isFood(id);
};
return fn;
});
2.在 getters:中定义,因为getters可以计算数据源的变化情况,但是也是需要返回一个函数,因为计算属性不能传参,返回函数来接收computed传的参数id
// 全局变量中的计算属性
// 计算属性不能传值,只能返回一个函数才可以传值
// 主要是判断变量中的id和传过来的id是否相同
isFood(state: any) {
return function (id: number) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == id) {
return true;
}
}
return false;
};
},
3.在getters中实时监听计算state中的数据源,就可以判断是否被添加到购物车了,主要是利用循环,判断每一项,数据源的商品id是否等于computed传过来的id,如果相同就说明添加了购物车,此时就显示数量,否则显示"添加购物车"
第六步: 获取数据,显示数据量,还是添加购物车
1.在getters定义方法,获取加入购物车的数量
//得到当前加减数量
getCount(state: any) {
return function (id: number) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == id) {
return state.cartList[i].count; //返回数量
}
}
return 1;
};
},
2.主要还是判断computed计算属性的参数和数据源的id是都相等
//获取数量
let getCount = computed(() => {
return function (id: any) {
return store.getters.getCount(id);
};
});
第七步: 实现加减数量的变化
1.给加减号添加点击事件,定义一个方法
// 给 加减号添加点击事件
<div>
<van-button size="mini" @click="updCount('+',ele.id)">+</van-button>
{{ getCount(ele.id) }}
<van-button size="mini" @click="updCount('-',ele.id)">-</van-button>
</div>
2.编写代码逻辑,但是要注意的是,mutations里面只能接受两个参数,但是我们要传递三个参数,此时用对象
// 在goods.vue中编写方法
// fh 符号 id就是商品的id
let updCount = (fh,id)=>{
let obj:Object = {
fh,
id
}
store.commit("updCount",obj)
}
3.在ts中编写代码
主要是利用循环遍历,state中每一项,判断我们传过去的id和实际数据源中的id是否一样;如果是加号就直接进行数量上相加,主要是当减法的时候,需要再做判断
// 购物车加减
updCount(state: any, obj: { fh: string; id: number }) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == obj.id) {
if (obj.fh == "+") {
state.cartList[i].count++;
} else {
// 需要判断
if (state.cartList[i].count > 1) {
state.cartList[i].count--;
} else {
// 如果减到0 将这个商品全局变量中删除 不能在我们购物车中显示了
state.cartList.splice(i, 1);
// 如果splice有2个参数的话,表示从i开始,一共删除几个元素
}
}
}
}
},
注意:如果数量此时已经不大于1了, 我们就要将这个商品在全局变量中删除,不能在购物车里显示,数组的方法,我们可以用splice,两个参数,第一个代表从i开始,第二个代表删除几个元素.
第八步:购物车弹框显示,已经添加到购物车的商品
1.在对应vant组件里面找模板,添加购物车,是在我们layout页面进行布局,因为商品组件,也要显示
<div class="footerbox">
<van-submit-bar :price="12312" button-text="去结算">
<van-action-bar-icon icon="cart-o" badge="5" size="large" @click="show = !show" />
</van-submit-bar>
<!-- // show其实就是一个开关 肯定是一个变量 那就要在js中定义 -->
<van-action-sheet v-model:show="show" title="标题">
<div class="content">内容</div>
</van-action-sheet>
</div>
js中
定义一个变量
// false 代表隐藏 true 代表的是显示
let show = ref(false)
2.我们需要定义一个方法,控制,点击按钮是否显示与隐藏
第九步:显示已选中的商品
问题:怎么去获取state中的值,这里面就是添加购物的商品
1.还是需要用getters来获取
// 这个是获取购物车中的list中数据
getCartList(state: any) {
return state.cartList;
},
2.用computed计算属性实时监听数据的变化,而且它还有缓存功能
let getCartList = computed(() => {
return store.getters.getCartList;
});
3.页面上显示但是会报错 getCount updCount 它们在goods.vue写过一次又要用, 复制粘贴过来
页面上使用
<van-card
v-for="(ele, i) in getCartList"
:key="i"
:price="ele.price.toFixed(2)"
:desc="ele.goodsDesc"
:title="ele.name"
:thumb="ele.imgUrl"
>
<template #footer>
<div>
<van-button size="mini" @click="updCount('+', ele.id)">+</van-button>
{{ getCount(ele.id) }}
<van-button size="mini" @click="updCount('-', ele.id)">-</van-button>
</div>
</template>
</van-card>
最后一步:计算总数量和总价格
1.触发点也是数据源的改变而触发 ,cartList数据改变,他就要触发
// 总价格和总数量
getTotal(state: any) {
let totalNumber: number = 0;
let totalPrice: number = 0;
// 循环把数量加起来 数量乘以价格
for (let i = 0; i < state.cartList.length; i++) {
totalNumber += state.cartList[i].count;
totalPrice += state.cartList[i].price * state.cartList[i].count;
}
return {
totalNumber,
totalPrice: totalPrice * 100, //还需要乘以100方便数据显示
};
},
},
// 获取总数量和总价格
let getTotal = computed(() => {
return store.getters.getTotal;
});
2.页面上直接渲染即可
最后总结:
1.其实里面很多方法,写起来非常的冗余,两个页面重复的代码比较多,这样会造成性能下限,资源浪费,所以我们可以把公共的代码提取出来
2.mixin他的作用就是可以把公共的方法提出来
第一步:
src中新建mixin的目录
第二步:
在mixin的目录中新建一个文件 cartMixin.ts
把公共的方法提出来
第三步:完整代码块
export default function () {
let store = useStore();
let getCartList = computed(() => {
return store.getters.getCartList;
});
// 获取总数量和总价格
let getTotal = computed(() => {
return store.getters.getTotal;
});
//第一个方法是把商品添加到购物车
let addToCart = (food: any) => {
//这个方法要操作 全局变量
// console.log(food);
store.commit("setData", food);
};
//他不是点击触发出的点击事件
//详情页面有一个添加购物车
//我点击详情页面的添加购物车,
//商品的按钮就会消失
//因为我已经添加了购物车
// 用什么方法去触发这个方法了
// 计算属性 具有监听的效果
// 当数据源改变的时候就会触发
// 计算属性能传值吗?
// 计算属性在vue中当基本属性使用
let isHaveFood = computed(() => {
let fn = (id: any) => {
//因为v-if最终的结果为true /false
// 我这里的方法会返回一个true /false
// 根据数据源的变化而变化
// 这个数据源在 全局变量中
// 实时的监听全局变量中的cartList的变化
//store.getters
return store.getters.isFood(id);
};
return fn;
});
//获取数量
let getCount = computed(() => {
return function (id: any) {
return store.getters.getCount(id);
};
});
//加或者减
//fh符号
let updCount = (fh: string, id: any) => {
// 因为加减是点击触发
// store中mutations去写方法
//mutations中的方法只有两个参数
// 第一个参数为state 第二个参数就是你传过去的
//我们有两个,怎么才能变成一个
let obj: Object = {
fh,
id,
};
store.commit("updCount", obj);
};
// 把方法暴露出去
return {
updCount,
getCount,
isHaveFood,
addToCart,
getTotal,
getCartList,
};
}
第四步:怎么在页面上用,按需引入即可使用
import cartMixin from "@/mixin/cartMixin.ts"
let { updCount, getCount, isHaveFood,addToCart,getTotal ,getCartList } = cartMixin()
cartMixin是一个方法 用的时候需要添加括号 cartMixin()
cartMixin() 代表的是调用这个方法 这个方法他又返回一个对象
````js
最后的最后:两个页面完整的代码复制在下面:
商品页面完整代码:
<template>
<div class="goodsbox">
<div class="leftbox">
<div>
<div :class="['nav', activeIndex == index ? 'active' : '']" v-for="(item, index) in data.goodsList" :key="index"
@click="toRight(index)">
{{ item.name }}
</div>
</div>
</div>
<div class="rightbox">
<div>
<div class="card" v-for="(i, e) in data.goodsList" :key="e" :id="'div' + e">
<div class="rightTitle">{{ i.name }}</div>
<van-card v-for="(ele, index) in i.foods" :key="index" :num="ele.sellCount" :price="ele.price.toFixed(2)"
:desc="ele.goodsDesc" :title="ele.name" :thumb="ele.imgUrl" @click="toDetail(ele)">
<!-- click-thumb 不给父级加 给这个图片加 然后跳转到详情 -->
<template #tags>
<span> 月售{{ ele.sellCount }} </span>
<span><van-rate v-model="ele.rating" :size="18" color="#ffd21e" void-icon="star" void-color="#eee"
readonly />
</span>
</template>
<template #footer>
<template v-if="isHaveFood(ele.id)">
<div>
<van-button size="mini" @click.stop="updCount('+', ele.id)">+</van-button>
{{ getCount(ele.id) }}
<van-button size="mini" @click.stop="updCount('-', ele.id)">-</van-button>
</div>
</template>
<van-button icon="plus" type="danger" v-else size="mini" round
@click.stop="addToCart(ele)">添加购物车</van-button>
</template>
</van-card>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import BetterScroll from 'better-scroll'
import { api_goodsList } from '../../api/home';
import { GoodsEntity } from '../../api/model/GoodsEntity';
import cartMixin from "../../mixin/cartMixin"
// 定义左侧右则滚动 放到全局作用域
let leftBScroll: BetterScroll;
let rightBScroll: BetterScroll;
// 定义一个数据源
let data = reactive(new GoodsEntity())
// 定义一个方法 可以不在onMounted中发请求
let queryGoodsList = async () => {
let res: any = await api_goodsList()
// console.log(res)
// 把拿到的值给自己定义的赋值
data.goodsList = res.data
nextTick(() => {
// 参数1是要滚动的元素 参数2是配置项
// as是原生js定义类型
leftBScroll = new BetterScroll(document.querySelector(".leftbox") as any, {
click: true,//允许滚动区域的选项可以点击
// disableMouse: true, //启用鼠标滚动
// disableTouch: true, //启用手指触摸
})
rightBScroll = new BetterScroll(document.querySelector(".rightbox") as any, {
click: true,//允许滚动区域的选项可以点击
probeType: 3,
// disableMouse: true, //启用鼠标滚动
// disableTouch: true, //启用手指触摸
// probeType: 3 //决定是否派发 scroll 事件
// 1. probeType 为 0,在任何时候都不派发 scroll 事件,
// 2. probeType 为 1,仅仅当手指按在滚动区域上,每隔 momentumLimitTime 毫秒派发一次 scroll 事件,
// 3. probeType 为 2,仅仅当手指按在滚动区域上,一直派发 scroll 事件,
// 4. probeType 为 3,任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画
})
// 正在滚动
rightBScroll.on("scroll", (val: any) => {
// val有2个值,就是滚动的高度
// console.log(val);
// Math.abs取绝对值 就是正值
let height = Math.abs(val.y);
// 得到滚动的距离
// 在正在滚动里面调用
// console.log(rightHeight.value)
for (let i = 0; i < rightHeight.value.length; i++) {
// 判断正在滚动的高度和实时监听的开始高度和结束高度
if (height >= rightHeight.value[i].startHeight && height <= rightHeight.value[i].endHeight) {
activeIndex.value = i;
// 提高性能
break; //结束循环
}
}
})
})
}
// 第三步算每一个div的区域
// 需要获取拿到三个值
// 需要实时监听 而且需要缓存 当数据源发生改变的时候,才会触发
let rightHeight = computed(() => {
let newArray = data.goodsList.map((ele, index) => {
// 获取右则的dom元素
let dom: any = document.querySelector("#div" + index)//做高亮使用
let startHeight = dom.offsetTop;//开始高度
let endHeight = dom.offsetTop + dom.offsetHeight;//结束高度
return { startHeight, endHeight, index }
})
return newArray //计算属性必须有返回值
})
// diao用方法
queryGoodsList();
//点击去高亮去右则
let activeIndex: any = ref(0)//默认第一个高亮
function toRight(index: number) {
// 通过下标点击左侧对应右则
// 先做左侧高亮 就是点击谁 谁添加类名active
activeIndex.value = index;
// el要谁显示在第一个位置的dom元素
// time 速度 单位为毫秒
// x x轴偏移量
// easing 速度曲线 匀速
let dom: any = document.querySelector("#div" + index)
// console.log(dom)
let eas: any = "easing"
rightBScroll.scrollToElement(dom, 3000, 0, -8, eas)
}
// 点击去详情
// food类型就是我们定义的Foods
// router和route是全局变量
let router = useRouter();//语法糖不需要new
function toDetail(food: any) {
// 编程式路由
router.push({
path: "/goodsDetail",
// query: food,
query: {
food: JSON.stringify(food)//需要深拷贝 因为拿到的值 数据只显示object
},
})
// router.push({
// name: "goodsDetail",
// params: food,
// // params: {
// // food: JSON.stringify(food)//需要深拷贝 因为拿到的值 数据只显示object
// // },
// })
}
// 购物车开始
// vue3中store用之前需要引入
// let store = useStore();
// function addToCart(food: any) {
// store.commit("setData", food);
// }
let { updCount,
getCount,
isHaveFood,
addToCart,
} = cartMixin()
// // 他不是点击触发的点击事件
// // 详情页面也有添加购物车
// // 点击了详情按钮就会消失 计算属性具有监听的效果
// let isHaveFood = computed(() => {
// // vue中计算属性不能传值 返回一个函数就可以传值
// return function (id: any) {
// // 因为v-if最终结果为true/false
// // 这里的方法返回一个true或者false
// // 通过false或者true判断添加购物车还是+ - 号
// // false或者true这个数据源在cartList
// // store.getters
// return store.getters.isFood(id);
// }
// })
// //得到当前加减数量 也要实时监听
// let getCount = computed(() => {
// return function (id: any) {
// return store.getters.getCount(id);
// }
// })
// // 加减 fh代表是符号
// let updCount = (fh: string, id: any) => {
// // 因为mutations只能传2个参数
// // 但是我们要传三个 只能定一个对象
// let obj: any = {
// fh,
// id,
// }
// store.commit("updCount", obj)
// }
</script>
<style scoped lang="scss">
.goodsbox {
width: 100%;
height: 100%;
display: flex;
.leftbox {
flex: 0 0 130px;
height: 100%;
background-color: rgb(229, 236, 197);
overflow-y: auto;
.nav {
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
}
.rightbox {
flex: 1;
height: 100%;
background-color: rgb(193, 227, 225);
overflow-y: auto;
// overflow: hidden;
.rightTitle {
margin-top: 5px;
}
}
.active {
color: #fff;
background: #f00;
}
}
</style>
Layout页面完整代码:
<template>
<div class="layoutheader">
<comm-header></comm-header>
</div>
<div class="navbox">
<van-tabs v-model:active="activeName" color="orange" title-active-color="#f00">
<!-- name一般就是路由 -->
<van-tab title="商品" name="/home/goods" to="/home/goods"></van-tab>
<van-tab title="评价" name="/home/ratings" to="/home/ratings"></van-tab>
<van-tab title="店铺" name="/home/shop" to="/home/shop"></van-tab>
</van-tabs>
</div>
<div class="content">
<router-view></router-view>
</div>
<div class="footerbox">
<van-submit-bar :price="getTotal.totalPrice" button-text="去结算" @click="show = !show"
style="z-index: 2100;background-color: orange;">
<van-action-bar-icon icon="cart-o" :badge="getTotal.totalNumber" />
</van-submit-bar>
<!-- 底部弹出层 -->
<van-action-sheet v-model:show="show" title="已加入的商品">
<div class="content">
<van-card v-for="(ele, i) in getCartList" :key="i" :price="ele.price.toFixed(2)" :desc="ele.goodsDesc"
:title="ele.name" :thumb="ele.imgUrl">
<template #footer>
<div>
<van-button size="mini" @click="updCount('+', ele.id)">+</van-button>
{{ getCount(ele.id) }}
<van-button size="mini" @click="updCount('-', ele.id)">-</van-button>
</div>
</template>
</van-card>
</div>
</van-action-sheet>
</div>
</template>
<script lang="ts" setup>
import commHeader from "../../components/commHeader/index.vue";
import cartMixin from "../../mixin/cartMixin"
// 初始值
const activeName = ref('/home/goods');
// 小bug 路由没用动 但是页面有一个初始值
// 实时监听临时路由地址的变化
// vue3中没有this.route 需要引入 已经自动导入
let route = useRoute()
watch(route, (n, o) => {
// 把新值赋值给路由
activeName.value = n.path
}, {
deep: true,
immediate: true,
})
// 开关 默认是隐藏状态
let show = ref(false)
// 实时监听计算属性中的数据变化
// 实时监听数据源的变化
// 计算属性 必须有返回值
let { updCount,
getCount,
getTotal,
getCartList, } = cartMixin()
</script>
<style lang="scss" scoped>
.layoutheader {
height: 150px;
}
.navbox {
height: 45px;
}
.content {
// 高度要减去上面导航和头部的高度 中间要加空格
height: calc(100% - 195px - 50px);
}
.footerbox {
height: 50px;
}
::v-deep .van-action-bar-icon__icon {
font-size: 26px;
}
::v-deep .van-popup {
height: 250px;
padding-bottom: 50px;
}
.van-action-bar-icon {
background-color: orange;
}
</style>
store下cart.ts完成代码:
export default {
state: {
// 全局变量
cartList: [], //定义一个数组,存你往购物车添加的所有商品
},
getters: {
// 全局变量中的计算属性
// 计算属性不能传值,只能返回一个函数才可以传值
// 主要是判断变量中的id和传过来的id是否相同
isFood(state: any) {
return function (id: number) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == id) {
return true;
}
}
return false;
};
},
//得到当前加减数量
getCount(state: any) {
return function (id: number) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == id) {
return state.cartList[i].count; //返回数量
}
}
return 1;
};
},
// 这个是获取购物车中的list中数据
getCartList(state: any) {
return state.cartList;
},
// 总价格和总数量
getTotal(state: any) {
let totalNumber: number = 0;
let totalPrice: number = 0;
// 循环把数量加起来 数量乘以价格
for (let i = 0; i < state.cartList.length; i++) {
totalNumber += state.cartList[i].count;
totalPrice += state.cartList[i].price * state.cartList[i].count;
}
return {
totalNumber,
totalPrice: totalPrice * 100, //还需要乘以100方便数据显示
};
},
},
mutations: {
// 同步的修改state的值的方法
setData(state: any, foodObj: any) {
// 要不加的数据追加到变量中
state.cartList.push({ count: 1, ...foodObj });
},
// 购物车加减
updCount(state: any, obj: { fh: string; id: number }) {
for (let i = 0; i < state.cartList.length; i++) {
if (state.cartList[i].id == obj.id) {
if (obj.fh == "+") {
state.cartList[i].count++;
} else {
// 需要判断
if (state.cartList[i].count > 1) {
state.cartList[i].count--;
} else {
// 如果减到0 将这个商品全局变量中删除 不能在我们购物车中显示了
state.cartList.splice(i, 1);
// 如果splice有2个参数的话,表示从i开始,一共删除几个元素
}
}
}
}
},
},
actions: {
// 异步方法 可以修改state
},
};
样式展示:
更多推荐
所有评论(0)