目录
购物车数据联动 💚
个人主页:小杰学前端
Vue3 京东到家项目实战第一篇:京东到家(首页及登录功能开发)
项目源码在文章最后哦🙋🙋🙋
在第一篇中我们的附近店铺是写死的数据,我们把它通过接口请求过来。
在 util 下的 request.js 中我们还需要写一个 get 请求来获取附近店铺的数据,那我们创建一个 axios 实例对象,在里面配置参数,这样就不需要在每个请求里都设置 baseURL 了。
- const instance = axios.create({
- baseURL: 'https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd',
- timeout: 10000
- })
这样我们把原来的 post 进行一下封装:
- export const post = (url, data = {}) => {
- return new Promise((resolve, reject) => {
- instance.post(url, data, {
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((response) => {
- resolve(response)
- }, err => {
- reject(err)
- })
- })
- }
然后再写一个 get 请求:
- export const get = (url, params = {}) => {
- return new Promise((resolve, reject) => {
- instance.get(url, { params }).then((response) => {
- resolve(response)
- }, err => {
- reject(err)
- })
- })
- }
现在附近店铺的数据我们是写在 setup 里:
现在我们就要改成从后端接口来请求相关数据 ,删掉这些数据然后把刚刚定义的get方法引入进来,向接口发送get请求,这里请求的接口在接口文档里:
我们调用封装的 get 请求,把url设置为热门店铺的接口地址 :
- import { ref } from 'vue'
- import { get } from '../../utils/request'
- export default ({
- name: 'NearBy',
- setup () {
- const nearbyList = ref([])
- const getNearbyList = async () => {
- const result = await get('/api/shop/hot-list')
- console.log(result)
- }
- getNearbyList()
- return {
- nearbyList,
- getNearbyList
- }
- }
- })
打印一下返回的结果,启动项目,查看控制台输出:
现在后端的数据就被我们接收到了。
我们修改一下 NearBy 中的代码:
- export default ({
- name: 'NearBy',
- setup () {
- // const router = useRouter()
- const nearbyList = ref([])
- const getNearbyList = async () => {
- const result = await get('/api/shop/hot-list')
- if (result.data.errno === 0) {
- nearbyList.value = result.data.data
- }
- }
- getNearbyList()
- return {
- nearbyList
- }
- }
- })
把原来的 nearblList 通过ref设置为响应式的数组,然后把 get 请求回来的数据赋给我们定义好的数组,当然视图模板里的内容也需要改动,我们先看一下接口信息:
在原来的视图模板中,循环列表里的信息时,key我们定义的是 id ,但是在后端给我们的接口数据结构中就不是 id 了,而是 _id,所以我们都得参照这个数据结构进行修改,修改后的代码如下:
- <div class="nearby">
- <h3 class="nearby__title">附近店铺h3>
- <div class="nearby__item" v-for="item in nearbyList" :key="item._id">
- <img :src="item.imgUrl" alt="" class="nearby__item__img">
- <div class="nearby__content">
- <div class="nearby__content__title">{{item.name}}div>
- <div class="nearby__content__tags">
- <span class="nearby__content__tag">月售:{{item.sales}}span>
- <span class="nearby__content__tag">起送:{{item.expressLimit}}span>
- <span class="nearby__content__tag">基础运费:{{item.expressPrice}}span>
- div>
- <p class="nearby__content__highlight">{{item.slogan}}p>
- div>
- div>
- div>
- template>
那么现在附近店铺显示的信息就是我们在后端返回过来的数据了:
我们为了让代码的逻辑性与可读性更高,在 setup 外面再定义一个函数然后把 get 请求的相关代码都放进去,在 setup 中再把这个函数的返回值引入进来:
- const useNearbyList = () => {
- const nearbyList = ref([])
- const getNearbyList = async () => {
- const result = await get('/api/shop/hot-list')
- if (result.data.errno === 0) {
- nearbyList.value = result.data.data
- }
- }
- return { nearbyList, getNearbyList }
- }
-
- export default ({
- name: 'NearBy',
- setup () {
- const { nearbyList, getNearbyList } = useNearbyList()
- getNearbyList()
- return {
- nearbyList
- }
- }
- })
这样我们的 setup 里只关心代码的实现逻辑而不需要关心内部的实现细节,这样代码的维护性和可读性就大大增强了。
这是我们要实现的效果图:
我们可以看到在顶部的内容和我们在附近店铺制作的内容是一样的,那我们就可以把附近店铺共用的部分做成一个组件,这样就可以在这块复用了。
首先我们先创建一个商家详情页面的路由,先进入 router 下的 index.js 配置路由信息:
- {
- path: '/shop',
- name: 'shop',
- component: () => import('../views/shop/ShopView')
- }
然后在 views 下创建这个组件:
下面我们就先把NearBy 里共用的部分摘离出来做成一个组件:
- <div class="shop">
- <img :src="item.imgUrl" alt="" class="shop__img">
- <div class="shop__content">
- <div class="shop__content__title">{{item.name}}div>
- <div class="shop__content__tags">
- <span class="shop__content__tag">月售:{{item.sales}}span>
- <span class="shop__content__tag">起送:{{item.expressLimit}}span>
- <span class="shop__content__tag">基础运费:{{item.expressPrice}}span>
- div>
- <p class="shop__content__highlight">{{item.slogan}}p>
- div>
- div>
- template>
-
- <script>
- export default {
- name: 'ShopInfo',
- props: ['item']
- }
-
- script>
- <style lang="scss" scoped>
- @import '../style/viriables.scss';
- .shop {
- display: flex;
- padding-top: .12rem;
- &__img {
- margin-right: .16rem;
- width: .56rem;
- height: .56rem;
- }
- &__content {
- padding-bottom: .12rem;
- border-bottom: 1px solid $content-fontcolor;
- &__title {
- line-height: .22rem;
- font-size: .16rem;
- color:$content-fontcolor ;
- }
- &__tags {
- margin-top: .08rem;
- line-height: .18rem;
- font-size: .13rem;
- color:$content-fontcolor ;
- }
- &__tag {
- margin-right: .16rem;
- }
- &__highlight {
- margin: .08rem 0 0 0;
- line-height: .18rem;
- font-size: .13rem;
- color: #E93B3B;
- }
- }
- }
- style>
我们把共用的模板和样式从 nearby 中抽离出来,并且修改相应的类名,因为我们在这个组件中需要用到 item ,我们需要从nearby组件中把它传过来,我们先在 Nearby 中引入子组件然后在视图模板中使用,并且把 item 传递给子组件:
- <div class="nearby">
- <h3 class="nearby__title">附近店铺h3>
- <shop-info v-for="item in nearbyList" :key="item._id" :item="item"/>
- div>
这样我们就成功创建了一个可复用的组件。
在商家详情页面中我们把这个组件引入进来,它的数据请求方式和 NearBy 中的一样,只不过接口是 /api/shop/:id,这里我们先请求沃尔玛商店的数据,所以写1就可以:
- <div class="wrapper">
- <shop-info :item="shopList" />
- div>
-
- <script>
- import { ref } from 'vue'
- import { get } from '../../utils/request'
- import ShopInfo from '../../components/ShopInfo.vue'
-
- const useShopList = () => {
- const shopList = ref({})
- const getShopList = async () => {
- const result = await get('/api/shop/1')
- if (result.data.errno === 0) {
- shopList.value = result.data.data
- }
- }
- return { shopList, getShopList }
- }
-
- export default {
- name: 'ShopView',
- components: { ShopInfo },
- setup () {
- const { shopList, getShopList } = useShopList()
- getShopList()
- return {
- shopList
- }
- }
- }
- script>
-
- <style lang="scss" scoped>
- .wrapper {
- padding: 0 .18rem;
- }
- style>
现在商家详情页面就显示出我们想要的内容了:
但是在我们的这个页面中,并没有下面的这个短线,也就是底边框,这个底边框样式是我们在做附近店铺的时候加的,所以我们也不能把他删掉,因为附近店铺中也需要,所以我们通过再设置一个属性来控制子组件中的样式,在子组件中边框样式那里多加一个类,当我们设置 --bordered 这个类为 false的时候就隐藏这个属性:
我们在 props 里再设置一个hideBorder属性来控制这个类 :
然后修改对应的视图模板里的语句:
<div :class="{'shop__content': true, 'shop__content--bordered': hideBorder ? false: true}">
当 hideBorder 属性为true的时候,'shop__content--bordered'这个类就是 false,就不显示这个属性。我们在商家详情页面传递这个属性,设置他为 true:
- <div class="wrapper">
- <shop-info :item="shopList" :hideBorder="true"/>
- div>
启动项目,查看效果:
现在我们的商家详情页面中就不显示下面的底边框了。
下面我们实现在首页单击沃尔玛和山姆会员商店时的跳转效果:
- <div class="nearby">
- <h3 class="nearby__title">附近店铺h3>
- <router-link to="/shop" v-for="item in nearbyList" :key="item._id">
- <shop-info :item="item" />
- router-link>
- div>
用 router-link 把它包裹起来,然后把循环指令放在 router-link 上就行。
但是当我们在首页单击沃尔玛或者山姆会员商店的时候,他会跳转到商品详情页面,但是显然沃尔玛和山姆会员商店的显示数据不一样,我们怎么实现这个功能呢?
在我们单击沃尔玛或山姆会员商店的时候,会把对应商店的 id 带到商品详情页面。所以下面我们就来修改路由路径:
- {
- path: '/shop/:id',
- name: 'shop',
- component: () => import('../views/shop/ShopView')
- }
修改 NearBy 中的 router-link 如下:
"`/shop/${item._id}`" v-for="item in nearbyList" :key="item._id"> - <shop-info :item="item" />
现在当我们单击不同的商店时,商店对应的 id 就会被带到商品详情页面,详情页面就会根据不同的 id 来获取对应的数据。
当我们单击沃尔玛时,路径中的参数 id 是1。
当我们单击山姆会员商店时,上面的id就是2了,这就达到我们想要的效果了:
现在要解决的问题就是,我如何根据参数来获取对应的数据呢,现在我们不论点击哪个商店拿到的都是 id 为 1的数据。
在路由里提供给我们了另一个方法来拿到路由中的参数,就是 useRoute。
我们把它引入进来:
import { useRoute } from 'vue-router'
通过 route.params.id 我们就可以拿到路由参数中 id 的值:
我们启动项目,当我们单击山姆会员商店时, 在控制台中查看输出:
这样我们就拿到了对应的 id 值
那我们就修改 get 请求中的参数:
await get(`/api/shop/${route.params.id}`)
这样当我们单击不同的商店时它返回的就是对应的数据了。由于本项目并不是真正的后端接口,所以每个商店返回的都是同样的数据,我们主要是学习这个过程,明白这个逻辑就行。
值得注意的一点是,当我们进入商家详情页面的时候还没有加载完成的图片会有一个空白的展示,
这样就显的很难看:
我们可以对代码进行一下优化,让元素在没有加载完成的时候不显示:
"shopList" :hideBorder="true" v-show="shopList.imgUrl"/>
这段代码的意思就是在图片没有加载完成时,就让 display为 none ,这样就不会显示了,我们再看一下效果:
在我们详情页面的样式中,多次使用到了 #333 这个颜色,那我们就把他放到 viriables 里,作为变量使用它:
还有一个值得注意的点是,如果番茄250g/份这里字数很多的话,他就会向下换行输出,像这样:
这样的话就会影响到下面的商品的布局,我们希望它不换行,多出的部分用省略号表示。
用这三行代码就能实现这个功能:
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
现在我们要实现点击左侧不同的分类,右侧展示不同的样式,这里的数据我们要通过向模拟的后端接口发送请求来实现,我们看一下接口文档,找到对应的接口地址:
我们点开它,看看接口的详细信息:
接口的 url 格式最后还带一个参数,比如我们点击的是全部商品,传递过去的 tab 值就是 all,点击秒杀传递的 tab 值就是 seckill,点击水果,传递的 tab 值就是 fruit,后端根据返回的这个参数来给我们对应的数据。
这里我们对这个接口发送 get 请求,看看返回的结果:
- setup () {
- const getContentData = async () => {
- const result = await get('/api/shop/1/products', {
- tab: 'all'
- })
- console.log(result)
- }
- getContentData()
- return {
- }
- }
启动项目,查看控制台输出:
现在我们商品详情页需要的数据就被成功获取到了,下面我们来完善我们的逻辑,现在我们要实现的是把请求的数据展示在页面上替换我们之前写死的数据。
这是我们之前在页面中展示的数据:
现在我们根据接口文档的参数来添加插值表达式:
我们设置一个对象来接收所有数据,并且判断如果数据没有错误,就把请求回来的数据赋给对象:
- setup () {
- const contentList = ref({})
- const getContentData = async () => {
- const result = await get('/api/shop/1/products', {
- tab: 'all'
- })
- if (result.data.errno === 0) {
- contentList.value = result.data.data
- }
- }
- getContentData()
- return {
- contentList
- }
- }
然后我们在视图模板中循环商品数据,根据返回数据的数据结构来写插值表达式:
- class="product__item" v-for="item in contentList" :key="item._id">
- <img class="product__item__img" src="http://www.dell-lee.com/imgs/vue3/near.png" alt="">
- <div class="product__item__detail">
- <h4 class="product__item__title">{{item.name}}h4>
- <p class="product__item__sales">月售 {{item.sales}} 件p>
- <p class="product__item__price">
- <span class="product__item__yen">¥span>{{item.price}}
- <span class="product__item__origin">¥{{item.oldPrice}}span>
- p>
- div>
- <div class="product__number">
- <span class="product__number__minus">-span>
- 0
- <span class="product__number__plus">+span>
- div>
- div>
启动项目,查看效果:
现在我们所看到的数据都是通过后端接口返回来的了,这里图片后面会改先这么做。
现在我们要实现的就是单击左侧不同的选项,能够请求不同的数据,所以我们应该把 tab 值作为参数发送给接口,因为我们刚加载这个页面的时候默认就是请求全部商品,所以我们这么修改代码:
- const getContentData = async (tab) => {
- const result = await get('/api/shop/1/products', { tab })
- if (result.data.errno === 0) {
- contentList.value = result.data.data
- }
- }
- getContentData('all')
当单击秒杀或新鲜水果的时候,会传递不同的 tab 值,所以我们应该给他们都绑定不同的事件。
因为我们需要给左侧每一个选项都绑定点击事件,那就把左侧的数据都放到数组中,通过 v-for 循环来展示数据并绑定事件。
我们定义一个 categories 对象数组来存储信息:
- const categories = [
- {
- name: '全部商品',
- tab: 'all'
- },
- {
- name: '秒杀',
- tab: 'seckill'
- },
- {
- name: '新鲜水果',
- tab: 'fruit'
- }
- ]
通过v-for 循环来绑定数据和事件:
class="category__item" v-for="item in categories" :key="item.name" @click="handleCategoryClick(item)">{{item.name}}
我们定义一个一个点击事件,然后输出点击的每个选项的 tab 值:
- const handleCategoryClick = (item) => {
- console.log(item.tab)
- }
启动项目,分别点击全部商品,秒杀和新鲜水果,可以看到控制台中顺序输出了对应的 tab 值:
那现在我们只需要在每个点击事件中调用 getContentData 方法就行:
- const handleCategoryClick = (item) => {
- getContentData(item.tab)
- }
这样当我们点击左侧不同选项时,获取的就是对应的数据了。
我们点击控制台的网络,查看请求内容,当我们分别点击全部商品,秒杀和新鲜水果时,发送的都是对应的 tab 值:
有的小伙伴在运行项目时发现数据并没有变化,是因为我们这里用的是 mock 平台,实际上并不是真实的数据,是模拟后端写死的数据,我们只是为了接口跑通,也就是说我们整套逻辑是一点问题没有的,只不过并不是真实后端,而是我们模拟的后端。
现在还有一个问题就是我们在 v-for 循环生成左侧的数据时,全部商品那里的背景应该是白色的,之前我们给了他一个类名,因为在 v-for 循环中加这个类名就会让所有选项背景都变成白色,所以我们给他删掉了,那现在我们怎么再把它加上呢?
通过动态绑定类名实现:
class="{'category__item':true, 'category__item--active': currentTab === item.tab}" v-for="item in categories" :key="item.name" @click="handleCategoryClick(item)">{{item.name}}
我们定义一个 currentTab 属性,在setup中设置他的值为 all,当循环的 tab 值是 all 时,就添加这个类名,这样我们这个效果就实现了。
我们把图片的 src 地址也和接口返回的图片地址绑定一下,这样就能生成不同的图片了:
启动项目,查看效果:
通过 watchEffect 巧妙的进行代码拆分
现在我们要尽量把 setup 中逻辑相同的代码进行拆分,让我们的代码具备更强的逻辑性和可维护性。
在上一节中我们通过绑定样式来实现全部商品背景颜色的变化,但是这里是有错误的,我们想实现的效果是点击哪个选项哪个选项的背景都会变色。我们把关于 tab 实现逻辑的代码都拆分出来,搬到 setup 的外面:
- const useTabEffect = () => {
- const currentTab = ref(categories[0].tab)
- const handleTabClick = (item) => {
- currentTab.value = item.tab
- }
- return { currentTab, handleTabClick }
- }
因为我们默认跳转到商品详情页面时,全部商品的背景色是白色,所以这里先定义 currentTab 为商品信息数组的第一个元素的 tab 值。然后我们把原来的 handleCategiryClick 改为 handleTabClick,把视图模板里的点击事件函数名也改成这个,因为我们这个函数里只关心 tab 逻辑的实现,所以函数名要一致。当点击左侧选项的时候就把这个选项的 tab 值赋给 currentTab的value值,然后我们再把数据和函数 return 出去,这样在 setup 里就能接收到了:
const { currentTab, handleTabClick } = useTabEffect()
现在我们把通过 get 请求获取数据的相关代码从 setup 里抽离出来,在外面定义一个新的函数:
- const getContentEffect = (currentTab) => {
- const route = useRoute()
- const shopId = route.params.id
- const contentList = ref({})
- const getContentData = async () => {
- const result = await get(`/api/shop/${shopId}/products`, { tab: currentTab.value })
- if (result.data.errno === 0) {
- contentList.value = result.data.data
- }
- }
- // getContentData('all')
- watchEffect(() => {
- getContentData(currentTab)
- })
- return { route, contentList, getContentData, shopId }
- }
在这里我们把 currentTab 作为参数传了进来,我们这里通过 watchEffect 来监听 getContentEffect 这个方法,在 watchEffect 中会立即执行传入一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。也就是说如果 currentTab 变了那么 getContentEffect 就会重新执行来获取数据,这不就是我们想要的么,这样整个代码的了逻辑性就非常强了,我们最后通过 return 把数据和函数 return 出来,在 setup 中调用。
我们看一下 setup 目前的代码:
- setup () {
- const { currentTab, handleTabClick } = useTabEffect()
- const { route, contentList, getContentData, shopId } = getContentEffect(currentTab)
- return {
- contentList,
- categories,
- handleTabClick,
- currentTab,
- getContentData,
- route,
- shopId
- }
- }
是不是非常简洁,比原来写的优雅多了,先在这个组件的可维护性就大大加强了。
底部购物车样式开发 💨
在 shop 目录中新建一个 CartFooter 组件来编写底部购物车:
我们在 ShopView 页面中引入这个子组件:
import CartFooter from './CartFooter.vue'
components: { ShopInfo, ContentShop, CartFooter }
在视图模板中用这个子组件:
我们先给 CartFooter 一个简单的样式:
- <div class="cart">cartdiv>
-
- <script>
- export default {
- }
- script>
-
- <style lang="scss" scoped>
- .cart {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- height: .5rem;
- background-color: pink;
- }
- style>
启动项目,看看输出的效果:
现在底部样式就能展示出来了,接下来我们对底部的内容做一个实现
先在视图中完成基本的 dom 结构:
- <div class="cart">
- <div class="check">
- <div class="check__icon">
- <img src="http://www.dell-lee.com/imgs/vue3/basket.png" alt="" class="check__icon__img">
- <div class="check__icon__tag">1div>
- div>
- <div class="check__info">
- 总计:<span class="check__info__price">¥128span>
- div>
- <div class="check__btn">去结算div>
- div>
- div>
- template>
然后编写底部的样式:
- .cart {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- }
- .check {
- display: flex;
- height: .49rem;
- border-top: .01rem solid #F1F1F1;
- &__icon {
- position: relative;
- width: .84rem;
- &__img {
- display: block;
- margin: .12rem auto;
- width: .28rem;
- height: .26rem;
- }
- &__tag {
- position: absolute;
- right: .2rem;
- top: .04rem;
- color: #fff;
- width: .2rem;
- height: .2rem;
- transform: scale(.5);
- background-color: #E93B3B;
- border-radius: 50%;
- font-size: .12rem;
- text-align: center;
- line-height: .2rem;
- }
- }
- &__info {
- flex: 1;
- margin: auto 0;
- color: #333;
- font-size: .12rem;
- &__price {
- color: #E93B3B;
- font-size: .18rem;
- }
- }
- &__btn {
- width: .98rem;
- line-height: .49rem;
- text-align: center;
- color: #fff;
- font-size: .14rem;
- background-color: #4FB0F9;
- }
- }
现在我们底部的样式开发就完成了,启动项目,查看一下效果:
购物车数据联动 💨
同步改变选中数字与金额
下面我们就开始实现功能,先从购物车开始,当单击商品的加减号时,对应的选中数字和总计金额会相应改变。
因为我们购物车的数据不仅在当前这个页面需要用到,在购物车,结算商品等其他页面都需要用到这些数据,所以我们应该把这些数据放在全局进行管理。这就需要 vuex 了。
在 state 仓库里我们存储购物车的数据,下面我们来看一下数据存储的结构:
- state: {
- cartList: {
- // 第一层级是商铺的 id
- 1: {
- // 第二层是商品内容及购物数量
- 111: {
- _id: '1',
- name: '番茄250g/份',
- imgUrl: 'http://www.dell-lee.com/imgs/vue3/tomato.png',
- sales: 10,
- price: 33.6,
- oldPrice: 39.6,
- count: 2
- },
- 222: {
-
- }
- },
- 2: {
-
- }
- }
- }
第一层指的是商铺的id,因为每个商铺的购物车数据肯定是分开的,不一样的,不可能我在A商铺买了东西,进入B商铺的时候购物车还有这个东西。第二层存储的是每个商铺的所有货物的信息,count指的就是购买的数量。
现在我们回到 ContentShop 组件中,我们先实现点击加减号的时候能够把商品加到购物车里:
再点击加减号的时候要把商品存到购物车里去,我们把相关逻辑封装到一个方法里
首先我们引入 vue-store:
import { useStore } from 'vuex'
定义一个 useCartEffect 函数来获取仓库里的购物车数据:
- const useCartEffect = () => {
- const store = useStore()
- const cartList = store.state.cartList
- return { cartList }
- }
在 setup 中使用这个方法:
const { cartList } = useCartEffect()
当我们点击加减号时,里面的数字是什么呢?
这里改变的就是对应商铺的对应商品的 count 值
{{cartList?.[shopId]?.[item._id]?.count || 0}}
这里先看看 state 里对应商品有没有 count 信息,如果没有的话就存储0
因为点击加减号时我们会把对应的信息存储到仓库中,那么就需要加上点击事件了:
class="product__number__plus" @click="()=>{addItemToCart(shopId, item._id, item)}">+
在 useCartEffect 函数中定义这个方法,我们先做一个简单的输出:
- const useCartEffect = () => {
- const store = useStore()
- const cartList = store.state.cartList
- const addItemToCart = (shopId, productId, productInfo) => {
- console.log(shopId, productId, productInfo)
- }
- return { cartList, addItemToCart }
- }
当我们进入沃尔玛店铺,先在全部商品里点击番茄那里的加号,再去秒杀这块点击车厘子那里的加号,看看控制台的输出:
这里第一个 1 就是代表商铺,第二个数字表示选择的左侧栏的内容,然后输出的就是选择的商品信息
通过 store.commit 同步修改 store 中的数据:
- const addItemToCart = (shopId, productId, productInfo) => {
- store.commit('addItemToCart', {
- shopId, productId, productInfo
- })
- }
然后在 stroe 的 mutations 中接收这个方法:
- mutations: {
- addItemToCart (state, payload) {
- const { shopId, productId, productInfo } = payload
- console.log(shopId, productId, productInfo)
- }
- }
启动项目,进入沃尔玛店铺,点击新鲜水果下的帝王蟹,看看控制台输出结果:
控制台能够正常输出,那我们就可以重新编写里面的逻辑了:
- addItemToCart (state, payload) {
- const { shopId, productId, productInfo } = payload
- let shopInfo = state.cartList[shopId]
- if (!shopInfo) {
- shopInfo = {}
- }
- let product = shopInfo[productId]
- if (!product) {
- product = productInfo
- product.count = 0
- }
- product.count += 1
- shopInfo[productId] = product
- state.cartList[shopId] = shopInfo
- }
现在我们的 cartList 就是一个对象,里面没有存储任何信息,因为只有用户在购物车里进行操作了,才会有数据的存储或改变。当用户点击加号时,先判断 cartList 里有没有存储商铺的信息,没有的话就存储一个空对象,再判断商铺里面有没有选择的商品的信息,没有的话就把传过来的 prodectInfo 进行赋值,再添加一个 count 属性用来统计数量。因为我们现在是增加的操作,所以最后一定得让数量加一,最后再把改变后的数据存储在 state 中。
启动项目,进入沃尔玛店铺点击商品信息的加号:
我们再返回山姆会员商店,看看对于另一个店铺有没有影响:
山姆会员商店里面的购物车数据都是0,这样我们想要的效果就实现了
我们给加号做完点击事件后,如果用同样的方法实现减号的功能显然可以,但是这样会非常冗余,我们就会写了很多重复的代码,下面我们对之前定义的函数做一下修改。
我们传进去一个数字,减号就是 -1,加号就是 1,这样就能区分开来了:
- class="product__number__minus" @click="()=>{changeItemToCart(shopId, item._id, item, -1)}">-
- {{cartList?.[shopId]?.[item._id]?.count || 0}}
- class="product__number__plus" @click="()=>{changeItemToCart(shopId, item._id, item, 1)}">+
修改对应代码中的内容:
- const useCartEffect = () => {
- const store = useStore()
- const cartList = store.state.cartList
- const changeItemToCart = (shopId, productId, productInfo, num) => {
- store.commit('changeItemToCart', {
- shopId, productId, productInfo, num
- })
- }
- return { cartList, changeItemToCart }
- }
修改 mutations 里的函数:
- changeItemToCart (state, payload) {
- const { shopId, productId, productInfo, num } = payload
- let shopInfo = state.cartList[shopId]
- if (!shopInfo) {
- shopInfo = {}
- }
- let product = shopInfo[productId]
- if (!product) {
- product = productInfo
- product.count = 0
- }
- product.count = product.count + num
- if (product.count < 0) { product.count = 0 }
- shopInfo[productId] = product
- state.cartList[shopId] = shopInfo
- }
注意因为商品数量不能为负数,所以 count 小于零的时候再把它赋为0就行。
现在我们开始做购物车右上角的数字:
每当购物车里的内容改变时,这里的数字也会相应变化。对他的操作在 CartFooter 组件中实现。这里我们通过计算属性来实现这里数字的动态变化。
这里并不难,就是获取 state 中对应店铺的数据,然后在 computed 中来个循环,循环商铺里的所有商品信息,把他们的 count 值加一起就行。
现在对商品数目做增减时购物车上的数字也会动态变化了:
我们现在用相同的逻辑做底部金额总计的功能:
- const price = computed(() => {
- let cartShop = cartCount[shopId]
- let pricesum = 0
- if (cartShop) {
- for (let i in cartShop) {
- const product = cartShop[i]
- pricesum += (product.price * product.count)
- }
- }
- return pricesum.toFixed(2)
- })
最后对金额用 toFiexed 方法是保留小数点后两位的意思。
我们启动项目,查看效果:
现在我们想要实现这种显示购物车内容的效果:
我们可以看到这块的样式和我们商品信息的样式一模一样,我们只需要把模板和样式 copy 到底部组件中就行
因为底部循环生成的数据是来自我们仓库中存储的数据,所以我们需要定义一个方法来获取商铺中商品信息的列表:
- const productList = computed(() => {
- let productList = cartCount[shopId] || []
- return productList
- })
这样我们循环的时候循环这个列表就行了:
这里我们只需要通过 item.count 就能实现单击上面商品的加减号,下面购物车里的数字也随着变化的效果:
可是现在我们点下面的加减号的时候上面不会跟着变,现在我们把这个反向逻辑也做一下:
- class="product__number__minus" @click="()=>{changeItemToCart(item, -1)}">-
- {{item.count || 0}}
- class="product__number__plus" @click="()=>{changeItemToCart(item, 1)}">+
在 setup 里定义这个点击事件:
- let changeItemToCart = (item, num) => {
- item.count += num
- if (item.count < 0) { item.count = 0 }
- }
现在我们点击下面的加减号时上面也可以动态变化了。
还有一个点就是如果我们给购物车中的数量减到0了,就取消这块内容的显示
有的同学可能会说在里面包一层 template ,通过 v-if 实现如果选择商品的数量等于0就不显示,但是这么做是有问题的,因为外面的盒子有一个 Padding 值,所以不显示的盒子也会有一块留白,像这样:
所以正确的做法是应该 v-for 循环 template ,然后 v-if 的是 div 盒子,这样就不会有留白了。
根据购物车选中状态实现计算金额
现在我们想在左边加上对勾的这个小图标,当我们往购物车里新增商品的时候默认是处于背景为蓝色的勾选状态,当处于非勾选状态时,改变商品的数额不会改变总计金额,只有商品是勾选状态我们才能统计金额 。
我们先在 mutations 的方法里添加条件,让点击加号的时候默认图标处于选定状态:
这里我们的对勾图标通过 v-if 来控制显示与隐藏,changechecked是对勾的点击事件函数,,传入店铺id和商品id,实现当点击的时候让对勾的状态改变。
我们定义这个点击事件:
- const changechecked = (shopId, productId) => {
- store.commit('changechecked', { shopId, productId })
- }
现在我们看一下效果:
清空购物车功能
首先我们先写一下全选和清空购物车的样式:
我们先完成清空购物车的功能,当单击清空购物车时可以把存储在 state 内的指定商家的数据都清空掉,我们先给它加个点击事件:
class="product__header__clear" @click="clearAll(shopId)">清空购物车
然后在购物车逻辑函数中编写这个函数:
- const clearAll = (shopId) => {
- store.commit('clearAll', { shopId })
- }
我们在 store 里的 mutations 中接收这个方法,并实现函数的功能:
- clearAll (state, payload) {
- let shopInfo = state.cartList[payload.shopId]
- shopInfo = {}
- state.cartList[payload.shopId] = shopInfo
- }
我们只要把商家的商品信息赋给一个空对象就行。
现在我们实现全选按钮功能,我们先看一下视图模板:
当 ifAll 等于 true 时,就显示背景颜色为蓝色的按钮,等于 false 时就显示背景为白色的按钮代表没有全选状态。
这里的 ifAll 我们通过计算属性实现,只要有商品没有被选择时,全选按钮就是非选择状态,只有当全部商品都被选择时,全选按钮才是选定状态:
- const ifAll = computed(() => {
- let all = true
- let cartShop = cartCount[shopId]
- if (cartShop) {
- for (let i in cartShop) {
- const product = cartShop[i]
- if (product.count > 0 && product.check === false) {
- all = false
- }
- }
- }
- return all
- })
我们还想实现当点击全选按钮时,让商品信息中所有未被选择的商品都变成选定状态,定义一个点击事件:
- const changeAll = (shopId) => {
- store.commit('changeAll', { shopId })
- }
我们在 store 里的 mutations 中接收这个方法,并实现函数的功能:
- changeAll (state, payload) {
- let shopInfo = state.cartList[payload.shopId]
- for (let i in shopInfo) {
- shopInfo[i].check = true
- }
- }
现在我们想在点击购物车时才显示购物车里的内容,我们在 setup 里定义一个响应式数据通过 v-if 来控制购物车内容的显示,一开始让他为 false,再给购物车加个点击事件,每次点击都让状态取反,这样就实现了我们想要的效果。
最后我们想在展示购物车中内容时,给盖住的部分加一个蒙层,当不显示购物车内容时蒙层就隐藏,类似这种效果:
我们在最外层加个盒子:
再给 mask 一个样式:
- .mask {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- z-index: 1;
- background: rgba(0,0,0,.5);
- }
这样就能实现我们的效果了,蒙层的显示与隐藏和购物车的显示与隐藏都是通过一个数据控制的。
还有一个问题就是当我们的购物车中没有商品时,现在还会勾选全选按钮和清空购物车这一栏,显然这是不应该显示的。
我们只需要把所有用到 showproduct 的地方再加个判断条件,让当 total 大于等于零的时候才能显示:
这样当购物车中没有商品时就不会显示清空购物车这一栏了。
本地存储保存购物车数据
最后每当页面刷新的时候,我们购物车中的数据都会丢失,我们用本地存储来让购物车的数据存在浏览器中,在 store 的index.js中定义本地存储的存储和获取函数:
- const setLocalStorage = (state) => {
- const { cartList } = state
- const cartListString = JSON.stringify(cartList)
- localStorage.cartList = cartListString
- }
- const getLocalStorage = () => {
- return JSON.parse(localStorage.cartList) || {}
- }
这样 state 中carList的值就可以通过 getLocalStorage 直接获取:
- state: {
- cartList: getLocalStorage()
- }
并且每当我们对 carList 里的内容有修改时,都调用 setLocalStorage 函数来本地存储数据。这样我们在刷新页面的时候,数据还会存在就不会销毁了。
这样我们的购物车的全部逻辑就都实现了,是不是也没有想象中的复杂呀
项目源码地址:
https://gitee.com/jie_shao1112/jingdong-homehttps://gitee.com/jie_shao1112/jingdong-home