vue小项目(二)—— 购物车的完整实现

购物车的完整实现

  • 1 需要提前了解的基础知识
  • 2 目录部署
  • 3 效果
  • 4 实现的具体功能
  • 5 实现的完整代码
      • (1) main.js
      • (2) Router.js
      • (3)store.js
      • (4)App.js
      • (5)List.vue
      • (6)Home.vue
      • (7)Product.vue
      • (8)Check.vue
      • (9)base.scss
  • 6 后记

1 需要提前了解的基础知识

本项目完全使用的是前端开发,没有使用到服务器,(前端开发的小伙伴可以放心看),前端使用到: vue, vue-cli, vuex, vue-router, axios,ES6,ES Module等技术

2 目录部署

  1. 最终实现目录

vue小项目(二)—— 购物车的完整实现_第1张图片

2 public目录
因为本项目没有使用到服务器,我们这里将数据放在本地,方便获取,list.json是待会页面需要获取的json文件,static/img/list下的是图片资源(也可以放在scr/assets/img下),我后面会把相关资源上传,大家下载即可
vue小项目(二)—— 购物车的完整实现_第2张图片
3 src目录
vue小项目(二)—— 购物车的完整实现_第3张图片
components 组件(支付组件,商品组件)
​ views 页面(首页面,商品列表页面)
​ router.js 路由
​ store.js store
​ App.vue 应用程序组件
​ main.js 入口文件
style/iconfot存放的是阿里的字体图标文件,使用方法请戳()

3 效果

1 列表页
vue小项目(二)—— 购物车的完整实现_第4张图片
2 购物车页面
vue小项目(二)—— 购物车的完整实现_第5张图片
3 实现整体效果

4 实现的具体功能

  1. (加入购物车)在列表页选择商品加入购物车,加入购物车后不能再次加入,再次点击按钮就是从购物车中删除该商品vue小项目(二)—— 购物车的完整实现_第6张图片

  2. (路由转换)在列表页加入购物车后点击下方的‘进入购物车页面’,即可以看到加入购物车的商品,如果购物车中没有商品,则无法进入购物车页面,在购物车页面点击头部‘购物车’,即可返回到商品列表页面vue小项目(二)—— 购物车的完整实现_第7张图片

  3. (全选,单选)在购物车页面,当点击商品左上角的圆圈即表示要购买的商品,再次点击即取消购买,点击底部的全选即表示购物车页面的所有商品都要购买,再次点击,则取消全选

  4. (价钱计算)只有加入购物车后并且点击购买的商品才能结算价钱vue小项目(二)—— 购物车的完整实现_第8张图片

  5. (数量添加,数量减少)在购物车页面,点击+号,即添加该商品,点击-号,即减少该商品在购物车中数量,当数量为1时,再点击-号即表示从购物车中删除该商品

  6. (移出购物车)在购物车页面点击每个商品右上角的垃圾桶图标,即表示从购物车中将该商品移出vue小项目(二)—— 购物车的完整实现_第9张图片

  7. (本地存储)使用本地存储,以至于刷新页面或者重新打开该页面,之前加入购物车的商品依旧在,不能用重新选择vue小项目(二)—— 购物车的完整实现_第10张图片

5 实现的完整代码

因为代码内容和页面比较多,为了方便大家理解,我把每一个文件里的代码全部都显示出来,里面有详细的注释供大家阅读,如果还有不明白的可以留言哦

(1) main.js

注意:

  1. 阿里矢量字体图标的使用可以戳
  2. element-ui中文官网:可以戳 element-ui中文官网
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from 'axios'
// 引入element-ui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

// 引入阿里字体图标
import '@/assets/style/iconfont/iconfont.css'
// 安装
Vue.prototype.$http = axios
Vue.use(ElementUI)

import { Message } from 'element-ui';
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')

// 路由跳转之前拦截,不能在购物车为空的情况下进入购物车页面
router.beforeEach((to, from, next) => {
    if( to.path === '/') {
        next()
    } else {
        let carList = JSON.parse(localStorage.getItem('car'))
        if (carList.length > 0) {
            next()
        } else {
            Message({
                message: "购物车空空如也,赶紧去添加吧!",
                type: 'error'
            })
            next('/')
        }
    }
})

(2) Router.js

import Vue from 'vue'
import Router from 'vue-router'
import List from '@/views/List.vue'

Vue.use(Router)

export default new Router({
    routes: [
        // List页面
        {
            path: '/',
            component: List
        },
        // 购物车页面
        {
            path: '/home',
            name: 'home',
            // 异步引入
            component: () => import('@/views/Home')
        },
    ]
})

(3)store.js

注意: 这里store.js是本项目中比较复杂的一个页面,主要是因为里面有很多同步消息和变量

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        // 购物车 本地存储,防止数据丢失
        car: localStorage["car"] ? JSON.parse(localStorage["car"]) : [],
        // 总价
        totlePrice: 0,
        // 全选状态
        all_selected: false,
        // 购物车中购买的数量
        payNum: 0,
        // list页面控制‘加入购物车按钮’的样式  本地存储,防止数据丢失
        isShow: localStorage["isShow"] ? JSON.parse(localStorage["isShow"]) : []
    },
    getters: {
        // 时时监听car的变化
        carList(state) {
            // 初始化全选状态
            if (state.all_selected) {
                state.car.forEach(item => item.isBuy = true)
            }
            return state.car;
        },
        // 购买的总价钱
        allPrice(state) {
            let totlePrice = 0
            state.car.forEach(item => {
                // 如果加入购物车后点击了购买按钮
                if (item.isBuy) {
                    totlePrice += item.num * item.price
                }
            })
            return state.totlePrice = totlePrice
        },
        // 购买的商品的总数量
        payLength(state) {
            let paylength = 0
            state.car.forEach(item => {
                if (item.isBuy) {
                    paylength += item.num
                }
            })
            return  state.payNum = paylength;
        }
    },
    // 同步消息
    mutations: {
        // 从购物车中删除该商品
        deleteProduct(state, id) {
            // 商品在购物车中索引值
            let index = state.car.findIndex(item => item._id === id)
            // 商品在isShow中的索引值
            let indexShow = state.isShow.findIndex(item =>item.id === id)
            // 将购物车中该商品的selected属性改成false
            state.car[index].selected = false;
            // 从购物车中删除该商品
            state.car.splice(index, 1)
            // 从本地存储的购物车中删除该商品(即将删除了该商品的购物车赋值给本地存储)
            localStorage.setItem('car', JSON.stringify(state.car))
            // console.log(5555,state.car, state.isShow[index].show)
            // 将isShow中该商品的show属性改成相反的值
            state.isShow[indexShow].show = !state.isShow[indexShow].show
            // 将isShow赋值给本地
            localStorage.setItem('isShow', JSON.stringify(state.isShow)) 

        },
        // 商品数量增加
        addNum(state, id) {
            let index = state.car.findIndex(item => item._id === id)
            if (index >= 0) {
                state.car[index].num ++;
            }
        },
        // 数量减少
        reduceNum(state, id) {
            let index = state.car.findIndex(item => item._id === id);
            let indexShow = state.isShow.findIndex(item =>item.id === id)
            console.log('indexShow', indexShow)
            // 当数量为1时,从购物车里面删除
            if (state.car[index].num === 1) {
                // 逻辑同deleteProduct
                state.car[index].selected = false;
                state.car.splice(index, 1)
                localStorage.setItem('car', JSON.stringify(state.car))
                state.isShow[indexShow].show = !state.isShow[indexShow].show
                localStorage.setItem('isShow', JSON.stringify(state.isShow)) 
            } else {
                state.car[index].num --;
            }
            
        },
        // 加入购物车
        addCar(state, data) {
            // 修改传递进来的商品的属性 数量为1,默认选中加入购物车,默认加入购物车后先不购买
            // 数据丢失
            // Object.assign(data, {num: 1, selected: true, isBuy: false})
            let datas = {
                title: data.title,
                num: 1,
                selected: true,
                isBuy: false,
                img: data.img,
                sales: data.sales,
                _id: data._id,
                price: data.price
            }
            // 在购物车中查找该商品
            let index = state.car.findIndex(item => item._id === data._id)
            // 如果返回-1,说明购物车中没有该商品,将其添加进购物车
            // 当购物车中已经有该商品了则不再继续往里面添加该商品
            if (index === -1) {
                state.car.push(datas)
            }
            // 将购物车中的商品存入本地存储中,为了之后做页面的路由拦截
            localStorage.setItem('car', JSON.stringify(state.car))
        },
        // 单选  选择是否购买该商品
        selectSingle(state, id) {
            // 查找购物车中该商品的的索引值
            let index = state.car.findIndex(item => item._id === id)
            // console.log(111, state.car[index].isBuy, state.car[index].selected)
            // 将购物车中该商品选择属性取反
            state.car[index].isBuy = !state.car[index].isBuy;
            // console.log(222, state.car[index].isBuy)
            // 判断是否全选(如果有一个是未选中,则flag为true,如果全选中则flag为false)
            let flag = state.car.some(item => item.isBuy === false)
            // 全选中
            if (!flag) {
                state.all_selected = true
            } else {
                // 没有全选中
                state.all_selected = false
            }
        },
        // 全选
        selectAll(state) {
            // 取消所有商品的isBuy
            if (state.all_selected) {
                state.car.forEach(item => item.isBuy = false)
            } else {
                // 将所有商品的isBuy属性改成true
                state.car.forEach(item => item.isBuy = true)
            }
            state.all_selected = !state.all_selected;
        }
    },
    actions: {
       
    }
})

(4)App.js

<template>
<div id="app">
    <router-view></router-view>
</div>
</template>
<style lang="scss">
* {
    margin:0;
    padding: 0;
    list-style: none;
}
</style>
<script>
export default {
}
</script>

(5)List.vue

注意: List页面主要复杂在根据商品是否加入了购物车来决定该页面商品的“加入购物车“按钮”的样式,另外更新或者再次进入该页面后要根据本地存储里面的购物车商品来决定改按钮的显隐

<template>
<div class="list">
        <div  class="header">商品列表</div>
    <ul class="ul">
        <li class="lis" v-for="item in data" :key="item._id">
            <div class="content">
                <img :src="item.img" alt="">
                <div class="info">
                    <h2 class="title">{{item.title}}</h2>
                    <div class="bottom-info">
                        <span class="price">{{item.price}}</span>
                        <div :class="{
                            addCar: !item.isShow,
                            plainCar: item.isShow}"
                              @touchend="item.isShow ? deleteProduct(item._id) : addCar(item)" 
                              @click="changeStyle(item._id)"
                        >{{item.isShow ? '离开购物车' : '加入购物车'}}</div>
                    </div>
                </div>
            </div>
            <router-link to="/home" class="to-home" tag="div">进入购物车页面</router-link>
        </li>
    </ul>
    
</div>
</template>
<style lang="scss" scoped>
@import '@/base.scss';
.list {
    background-color: #efefef;
    padding: 5px 15px;
    border-radius: 10px;
    margin-bottom: 15px;
    .header {
        background-color: #f30;
        color: #fff;
        text-align: center;
        height: 60px;
        font-size: 28px;
        line-height: 60px;
        position: fixed;
        top: 0;
        right: 0;
        left: 0;
        margin-bottom: 60px;
    }
    ul {
        margin-top: 60px;
        margin-bottom: 40px;
        .lis {
            padding: 5px 8px;
            margin-bottom: 10px;
            background-color: #fff;
            .top-info {
                display: flex;
                margin-bottom: 8px;
                .icon-yuancircle46,
                .icon-xuanze {
                    font-size: 26px;
                    margin-right: 10px;
                }
                .name {
                    flex: 1;
                    font-size: 18px;
                    padding-top: 2px;
                }
                .icon-lajitong {
                    font-size: 24px;
                }
            }
            .content {
                display: flex;
                img {
                    width: 105px;
                    height: 90px;
                }
                .info {
                    margin-left: 15px;
                    flex: 1;
                    .title {
                        font-size: 18px;
                        font-weight: normal;
                        height: 65px;
                    }
                    .bottom-info {
                        display: flex;
                        .price {
                            color: $navColor;
                            flex: 1;
                            padding-top: 4px;
                        }
                        .plainCar {
                            padding: 5px 10px;
                            border-radius: 5px;
                            background-color: #ccc;
                            color: rgb(97, 92, 92);
                        }
                        .addCar {
                            padding: 5px 10px;
                            background-color: #f30;
                            border-radius: 5px;
                            color: #fff;
                        }
                    }
                }
        }
    }
}
    .to-home {
        height: 40px;
        line-height: 40px;
        padding: 5px 15px ;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        text-align: center;
        background-color: #fff;
        border-top: 1px solid #ccc;
        color: #f30;
        font-size: 26px;
    }
}
</style>
<script>
import { mapMutations, mapState } from 'vuex';
export default {
    data() {
        return {
            data: [],
        }
    },
    computed: {
        // 映射store里面的isShow,待会在本文件中可以直接使用
        ...mapState(['isShow'])
    },
    methods: {
        // 映射store中的同步消息,待会在本文件中可以直接使用
        ...mapMutations(['addCar', 'deleteProduct']),
        changeStyle(id) {
            // 点击‘加入购物车按钮’
            // 遍历商品列表 判断该商品是否在购物车中
            this.data.forEach((item, index) => {
                // 如果该商品在购物车中
                if (item._id === id) {
                    // 将其样式取反
                    item.isShow = !item.isShow
                    // 将该商品的id和是否显隐存储
                    let showItem = { id: item._id, show: item.isShow}
                    // 将该商品列表和isShow一一对应,方便后边进行查找
                    this.isShow[index] = showItem
                    // 将store中isShow存储到本地
                    localStorage.setItem('isShow', JSON.stringify(this.isShow))  
                } else {
                    // 如果该商品不在购物车中,首先判断本商品在本地isShow数组中有值,如果有值则不改变,如果没有值则添加默认值
                    if (!JSON.parse(localStorage.getItem('isShow'))[index]) {
                        let showItem = {index, id: "", show: false}
                        this.isShow[index] = showItem
                        localStorage.setItem('isShow', JSON.stringify(this.isShow)) 
                    }
                }
            })
        }
    },
    // 组件创建完成请求数据
    created() {
        this.$http.get('/data/list.json')
            .then(
                ({data}) => {
                    // 将获取的数据存储
                    this.data = data
                    // 获取本地存储的isShow
                    let showArr = JSON.parse(localStorage.getItem('isShow'))
                    this.data.forEach((item,index) => {
                        // 给获取到的商品列表每一个对象添加isShow属性,用于决定该商品中“加入购物车”按钮的样式
                        // 默认是显示‘加入购物车’的,如果本地存储中该商品有值,则显示本地存储中该商品的样式
                        this.$set(item, 'isShow', (showArr == null || showArr[index] == null) ? false : showArr[index].show)
                        }
                        )
                },
                err => console.log(err)
            )
    }
}
</script>

(6)Home.vue

注意: Home页面中引入Product和Check组件

<template>
<div class="home">
        <router-link to="/" class="header" tag="div">购物车</router-link>
        <Product v-for="item in carList" :key="item._id" :data="item"></Product>
        <!-- <Product v-for="item in $store.state.car" :key="item._id" :data="item"></Product> -->
        <Check></Check>
</div>
</template>
<style lang="scss">
@import '../base.scss';
body,
html {
    background-color: #efefef;
}
.home {
    margin-top: 60px;
    padding: 10px 15px;
    margin-bottom: 60px;
    .header {
        background-color: $navColor;
        color: #fff;
        text-align: center;
        height: 60px;
        font-size: 28px;
        line-height: 60px;
        position: fixed;
        top: 0;
        right: 0;
        left: 0;
        margin-bottom: 60px;
    }
}
</style>
<script>
import Check from '@/components/Check'
import Product from '@/components/Product'
import { mapGetters } from 'vuex'
export default {
    components: {Product, Check},
    data() {
        return {
        }
    },
    computed: {
        ...mapGetters(['carList'])
    }
   }
</script> 

(7)Product.vue

<template>
<div class="product">
    <div class="top-info">
        <i class="iconfont icon-xuanze" v-if="data.isBuy" @touchend="selectSingle(data._id)"></i>
        <i class="iconfont icon-yuancircle46" v-else @touchend="selectSingle(data._id)"></i>
        <p class="name">商家:以沫wh</p>
        <i class="iconfont icon-lajitong" @touchend="deleteProduct(data._id)"></i>
    </div>
    <div class="content">
        <img :src="data.img" alt="">
        <div class="info">
            <h2 class="title">{{data.title}}</h2>
            <div class="bottom-info">
                <span class="price">{{data.price}}</span>
                <div class="num">
                    <i class="iconfont icon-jianshao" @touchend="reduceNum(data._id)"></i>
                    <div class="sales">{{data.num}}</div>
                    <i class="iconfont icon-tianjia-copy" @touchend="addNum(data._id)"></i>
                </div>
            </div>
        </div>
    </div>
</div>
</template>
<style lang="scss" scoped>
@import '@/base.scss';
.product {
    background-color: #fff;
    padding: 15px;
    border-radius: 10px;
    margin-bottom: 15px;
    .top-info {
        display: flex;
        margin-bottom: 8px;
        .icon-yuancircle46,
        .icon-xuanze {
            font-size: 26px;
            margin-right: 10px;
        }
        .name {
            flex: 1;
            font-size: 18px;
            padding-top: 2px;
        }
        .icon-lajitong {
            font-size: 24px;
        }
    }
    .content {
        display: flex;
        img {
            width: 105px;
            height: 90px;
        }
        .info {
            margin-left: 15px;
            flex: 1;
            .title {
                font-size: 18px;
                font-weight: normal;
                height: 65px;
            }
            .bottom-info {
                display: flex;
                .price {
                    color: $navColor;
                    flex: 1;
                    padding-top: 4px;
                }
                .num {
                    display: flex;
                    .icon-jianshao,
                    .icon-tianjia-copy {
                        font-size: 25px;
                    }
                    .sales {
                        padding: 4px 8px;
                    }
                }
            }
        }
    }
}
</style>
<script>
import { mapMutations } from 'vuex';
export default {
    props: ['data'],
    data() {
        return {}
    },
    methods: {
        ...mapMutations(['selectSingle', 'addNum', 'reduceNum', 'deleteProduct'])
    },
    created() {
        // console.log('isBuy', this.data.isBuy, this.data.selected)
    }
}
</script>

(8)Check.vue

<template>
<div class="check">
    <div class="isSelAll" v-if="$store.state.all_selected">
        <i class="iconfont icon-xuanze"></i> 
        <span class="cancelSelAll" @touchend="selectAll()">取消全选</span>
    </div>
    <div class="isSelAll" v-else>
        <i class="iconfont icon-yuancircle46" ></i>
        <span class="selAll" @touchend="selectAll()">全选</span>
    </div>
    <div class="totlePrice">共计:{{$store.getters.allPrice}}</div>
    <div class="pay">结算({{$store.getters.payLength}})</div>
</div>
</template>
<style lang="scss">
@import '@/base.scss';
.check {
    height: 50px;
    line-height: 50px;
    padding: 5px 15px ;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    background-color: #fff;
    border-top: 1px solid #ccc;
    .isSelAll {
        flex: 1;
        .icon-xuanze,
        .icon-yuancircle46 {
            font-size: 26px;
            vertical-align: sub;
        }
        .cancelSelAll,
        .selAll {
            padding-left: 8px;
            font-size: 18px;
            color: $navColor;
            }
    }
    .totlePrice {
        font-size: 20px;
        margin-right: 10px;
        margin-top: 1px;
    }
    .pay {
        width: 80px;
        height: 40px;
        line-height: 40px;
        text-align: center;
        background-color: #f60;
        color: #fff;
        padding: 0px 10px;
        border-radius: 20px;
        font-size: 17px;
        margin-top: 5px;
    }
}
</style>
<script>
import {mapMutations} from 'vuex';
export default {
    methods: {
        ...mapMutations(['selectAll'])
    }
}
</script>

(9)base.scss

$navColor:  blue;
// 阿里字体图标设置
.icon, .iconfont {
  color: $navColor;
    font-family:"iconfont" !important;
    font-size:16px;
    font-style:normal;
    -webkit-font-smoothing: antialiased;
    -webkit-text-stroke-width: 0.2px;
    -moz-osx-font-smoothing: grayscale;
  }

6 后记

本项目到此也就全部完成了,基本实现了购物车的所有功能,因为也是在学习中,若果发现本文有错误请留言指正,我们一起进步呀!!
绝对帮到自己的小伙伴可以点个赞呦~

你可能感兴趣的:(vue小项目,javascript,css,vue.js,html5,es6)