手写手机端商城和购物车(vue)

245 阅读4分钟

采用cli方式,手机端是上下结构,底部tab,上面内容,内容主要包括首页(轮播,列表商品),商品详情页(可以添加到购物车,这里需要在底部tab和购物车页面同步展示添加的商品,层层props传值太麻烦,所以直接存到响应式vuex里面),购物车清算页,还有我的信息。请求用axios

首页列表数据太多最好缓存一下keep-alive,同时可以通过vuex的计算属性getNavState通过mapGetters辅助函数导出去判断是否展示底部导航

 <!-- app.vue 主体内容  keep-alive缓存 当前只缓存首页,因为首页东西很多-->
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
    <!-- 底部导航 -->
    <TabBottom  v-show="getNavState" />
// app.vue js部分
import {mapGetters} from 'vuex'  // 辅助函数
computed:{
    ...mapGetters(['getNavState']),
    num(){
      // 其他计算属性
    }
},
components:{
    TabBottom
}

底部导航页面页需要对购物车数量进行展示,通过mapGetters拿到vuexgetter计算属性totalNum来展示,为0就不展示

<!-- 底部导航TabBottom.vue -->
<template>
    <div class="nav-bottom">
      <router-link to="/home">
          <i class="iconfont s"></i>
          <label>首页</label>
      </router-link>
      <router-link to="/order">
          <i class="iconfont x"></i>
          <i v-if="totalNum>0">{{totalNum}}</i>
          <label>购物车</label>
      </router-link>
    </div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
  data(){
    return {
      msg:{}
    }
  },
  computed:mapGetters(['totalNum']),
  methods:{
  },
}
</script>
<style scoped>
    .active {
        color:red;
    }
</style>

首页图片太多也需要懒加载vue-lazyload

// 懒加载动态图片 固定静态图片用字体图标
yarn add vue-lazyload -S
yarn add axios -S
// main.js
import VueLazyload from 'vue-lazyload'  //懒加载
Vue.use(VueLazyload);
// 使用的组件list.vue 初始化访问固定@/assets/img/loading.gif图片
<img src="@/assets/img/loading.gif" v-lazy="item.url">

首页通常有轮播(每隔一定时间展示后面一张图(num++),找到最后一张图之后又从第一张开始num=0,这里适合用v-show)可以提到公共组件用props传入数据展示,大家一起用,轮播在组件挂载mounted时设置定时器setInterval播放autoPlay,同时需要在组件destroyed时移除定时器clearInterval

<template>
  <div class="banner">
    <img  v-for="(v,i) in img" :key="i" :src="v" v-show="nowNum==i" />
    <div class="banner-circle">
      <ul>
        <li :class="nowNum==i ?'selected':'' "  v-for="(v,i) in img" :key="i"></li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return {
      nowNum:0,// 控制当前展示那个baner图
      timer:null,// 定时器,控制多久播放,组件destroy的时候清除
    }
  },
  props:{
      img: { 
          type: Array, // 对象或数组默认值必须从一个工厂函数获取 
          default: function () { 
              return []
          }
      },
  },
  methods:{
    play(){
      this.timer = setInterval(this.autoPlay,2000);
    },
    autoPlay(){
      this.nowNum++;
      if(this.nowNum >=this.img.length){
        this.nowNum = 0;
      }
    }
  },
  mounted(){  //挂载完成
    this.play();
  },
  destroyed(){   //销毁
    clearInterval(this.timer);
  }
}
</script>

<style scoped>
...
</style>

router部分,主要是VueRouter,这里也用到了缓存组件keepAlive和匹配样式linkExactActiveClass

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)// 符合vue规范的就需要use一下
const routes = [
  {
    path: '/',
    redirect:'/home',// /重定向到首页
  },
  {
    path: '/home', //首页
    name: 'home',
    meta:{
      keepAlive:true ,  //需要缓存
      title:'分页'
    },
    component: () => import( '../views/module/Home/index.vue')
  },
  {
    path: '/detail/:id', //详情页
    name: 'detail',
    component: () => import( '../views/module/Detail/index.vue')
  },
  ....
]
const router = new VueRouter({
  linkActiveClass:'active',// 或者 linkExactActiveClass 精确匹配样式
  routes
})
export default router

axios请求处理

// request/index.js 导出到main.js使用
import axios from 'axios'
axios.defaults.baseURL = `http://xxx:8080`;
// 添加请求拦截器
axios.interceptors.request.use(config=>{
    // 加header,动画处理什么什么的
    return config
})
// 添加响应拦截器
axios.interceptors.response.use(response=>{
    // 返回响应码处理,token存储,服务器时间header里面获取date去存在vuex对比过期时间等
    return response
},err=>{
    // 对响应错误做点什么
   return Promise.reject(err);
})
export default axios

添加的具体业务请求

// request/moduels/list.js
import axios from '../moduels/index.js'  // from '.'等同于 './index'
export function getList(pageNo,pageSize){  //分页列表数据
    return axios({
        method:'get',
        url:`/home/page/${pageNo}/${pageSize}`
    })
}
export function getTotal(){  //所有列表的数据
    return axios({
        method:'get',
        url:`/home`
    })
}
...

store部分

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

Vue.use(Vuex)

var state = {
  isNavShow:true, //底部导航显示与隐藏
  cartAddData:[], //购物车数据
}
var getters={
  getNavState(state){  //获取底部导航状态的值
    return state.isNavShow
  },
  cartData(state){
    return state.cartAddData// 购物车数据
  },
  totalNum(state){
    return state.cartAddData.reduce((total,now)=>total+now.count,0)// 购物车总数
  },
   //  (state, getters)或者也可以通过getter返回一个函数,来实现getter传参,price为单价
  totalMoney: (state) => (price) => {
    return state.cartAddData.reduce((total,now)=>total+now.count*price,0)// 购物车总金额
  }
}
var mutations ={
  ADD(state){
    state.num +=1;
  },
  SHOWNAV(state){  //显示
    state.isNavShow = true;
  },
  HIDENAV(state){  //隐藏
    state.isNavShow = false;
  },
  CARTADD(state,data){  //购物车数据添加
    if(state.cartAddData.length>0){
      let item = state.cartAddData.filter(i => i.product_id===data.product_id)// 根据id对比cartAddData数据中是否有等于当前添加的数据
      if(item.length>0){
        item[0].count+=1// 购物车的数据加1
        return
      }else{
        // 没有匹配id的数据
        state.cartAddData.push(data);// 直接添加
      }
    }else{
      state.cartAddData.push(data);
    }
  }
}
var actions= {
  ADD({commit}){
    commit('ADD')
  },
  SHOWNAV({commit}){  //显示
    commit('SHOWNAV')
  },
  HIDENAV({commit}){  //隐藏
    commit('HIDENAV')
  },
  CARTADD({commit},data){  //购物车数据添加
    commit('CARTADD',data)
  },
}
export default new Vuex.Store({
  state,
  getters,
  mutations,
  actions
})

详情页,这里需要注意进入就隐藏底部tab和添加数据vuex后跳转router到购物车,每次添加商品时,需要用$set()添加商品数量让购物车页渲染

// detail.vue
<template>
  <div class="detail">
    <div>
      <a class="goback" @click="goBack()">返回</a>
      <div>
        <img :src="item.url" /> 
      </div>
      <div>
        <p>{{item.name}}</p>
        <p>{{item.detail}}</p>
      </div>
    </div>
    <div class="shoppCart" >
        <button @click="addToCart()">加入购物车</button>
    </div>
  </div>
</template>

<script>
import {getDetail} from '@/request/xxx'
export default {
  data(){
    return {
      item:{}
    }
  },
  methods:{
    getData(id=1){
       getDetail({id}).then(res=>{
         this.item = res?res:{}// 假设这就是返回的对象
       })
    },
    goBack(){
      this.$router.push('/home');// 返回到首页
    },
    addToCart(){
        // 每次添加商品时,需要添加商品数量
        this.$set(this.item,'count',1)// 需要用set去操作对象让页面监测到渲染
      this.$store.dispatch('CARTADD',this.item);// 添加商品
      this.$router.push('/order');// 跳转到购物车页面tab
    }
  },
  mounted(){
    this.getData(this.$route.params.id);
    // 这里需要router设置path: '/detail/:id', 才能拿到params下的id,否则用query方式路由传参
    this.$store.dispatch('HIDENAV');// 详情页需要隐藏底部导航
  },
  destroyed(){
    this.$store.dispatch('SHOWNAV');// 离开详情页需要显示底部导航
  }
}
</script>

购物车页面,vuex里面的geter计算属性来计算总价和总数量,actions操作全选,添加/减少商品数量

<template>
  <div class="order">
    <ul>
      <li v-for="(item, index) in cartData" :key="index">
       <div class="flex">
        <div class="shoppCart-list-l">
          <input type="checkbox" />
          <img :src="item.url" alt=""/>
        </div>
        <div class="shoppCart-list-r">
          <p>{{item.product_name}}</p>
          <div class="flex">
            <div>
              <span class="price-text">原价{{item.product_price}}</span> 
              <span class="red"> {{item.product_uprice}}</span>
            </div>
          <label>
            <button @click="countChange(item,'-')">-</button>{{item.count}}<button @click="countChange(item,'+')">+</button>
          </label>
          </div>
          
        </div>
        </div>
      </li>
    </ul>
     <div class="shoppCart-total">
          <div class="flex">
            <div><input type="checkbox" />全选</div>
            <label>
              <span>合计:</span>
              <b class="red">{{totalMoney(100)}}</b>
              <button class="btn">结算</button>
            </label>
          </div>
      </div>
  </div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
  data(){
    return {
      msg:{}
    }
  },
  computed:mapGetters(['cartData','totalMoney']),
  methods:{
    // ...mapActions(['ADD','SHOWNAV']),
    add(){
      // this.$store.dispatch('ADD');
    },
    // 有时间再来优化
    countChange(item,type){
      switch(type){
        case '+':
              item.count+=1;
              break;
        case '-':
              item.count-=1;
              break;
        default:
            return
      }
    }

  },
}
</script>
<style scoped>
.flex{
  display:flex;
  justify-content: space-between;
  margin:5%;
}
.shoppCart-list-l{
  display:flex;
}
.shoppCart-list-l img{
    height:4rem;
    width:4rem
  }
.red{
  color: red;
}
.price-text{
 text-decoration:line-through
}
.order{
  position:relative;
  height: 100vh;
}
.shoppCart-total{
  position: absolute;
  bottom: 4rem;
  width:100%;
}
</style>


// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'// 路由的引入
import store from './store'// vuex状态管理的引入
import VueLazyload from 'vue-lazyload'  //懒加载动态图片
import './assets/font/iconfont.css'// 字体图样式
import './components/global' //引入全局组件定义banner,这里少用还是,毕竟main一开始就会被加载
Vue.use(VueLazyload);
Vue.config.productionTip = false// 一些productionTip禁掉

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')