排序的操作

  • 要求的数据格式
- 数据格式说明
    - '1'表示'综合排序'
    - '2'表示'价格排序'
    - 'asc'表示'升序'
    - 'desc'表示降序
    
- 项目的数据格式的样子
	- 1:asc
	- 1:desc
	- 2:asc
	- 2:desc
### Search.index.vue
......
"order": "1:desc", // 项目的默认排序(综合降序排列)
  • class=active样式的显示: 由order参数值来决定
### Search.index.vue
......
<ul class="sui-nav">
  <!--找到'1'就展示class样式-->
  <li :class="{active:searchParams.order.indexOf('1') != -1}">
    <a href="#">综合</a>
  </li>
  <!--找到'2'就展示class样式-->
  <!--'1'和'2'不可能同时存在-->
  <li :class="{active:searchParams.order.indexOf('2') != -1}">
    <a href="#">价格⬆</a>
  </li>
</ul>
  • 简化一下代码,修改成计算属性的形式
computed:{
 ......
  isOne(){
    return this.searchParams.order.indexOf('1') != -1
  },
  isTwo(){
    return this.searchParams.order.indexOf('2') != -1
  }
},

......
<ul class="sui-nav">
  <li :class="{active:isOne}"> <!--应用-->
    <a href="#">综合</a>
  </li>
  <li :class="{active:isTwo}">
    <a href="#">价格</a>
  </li>
</ul>
  • 在线引入阿里icon图标库
### public.index.html
<head>
  ......
  <!--先引入全局css样式(阿里后台有提供代码)-->
  <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_4064138_5e7pv6qsh5g.css">
</head>

### 举例使用
......
<span class="iconfont icon-down"></span>

  • 本项目中,我们引入两个箭头(向上升 && 向下降)
- 设计思路: 
	- 当 order值包含'asc'时,就展示'上升箭头'
	- 当 order值包含'desc'时,就展示'下降箭头'
<ul class="sui-nav">
  <li :class="{active:isOne}">
  	<!--展示的时候,添加两个class样式: iconfont && icon-xxx -->
  	<!--语法注意事项: 如果不加引号,icon-down写法会报'-'错误-->
    <a href="#">综合<span v-show="isOne" class="iconfont" :class="{'icon-down':isDesc,'icon-ttd-copy':isAsc}"></span></a>
  </li>
  <li :class="{active:isTwo}">
    <a href="#">价格<span v-show="isTwo" class="iconfont" :class="{'icon-down':isDesc,'icon-ttd-copy':isAsc}"></span></a>
  </li>
</ul>
......
computed:{
  
  isOne(){
    return this.searchParams.order.indexOf('1') != -1
  },
  isTwo(){
    return this.searchParams.order.indexOf('2') != -1
  },
  isAsc(){
    // 若找到asc
    return this.searchParams.order.indexOf('asc') != -1
  },
  // 若找到desc
  isDesc(){
    return this.searchParams.order.indexOf('desc') != -1
  }
},
  • 字段排序操作: 绑定点击事件并写逻辑
### Search.index.vue
......
<div class="navbar-inner filter">
  <ul class="sui-nav">
    <li :class="{active:isOne}" @click="changeOrder('1')"> <!--绑定两个点击事件并自定义传参-->
     ......
    </li>
    <li :class="{active:isTwo}" @click="changeOrder('2')">
     ......
    </li>
  </ul>
</div>
......
changeOrder(flag){
  // 数据格式: "1:asc"
  let originOrder = this.searchParams.order;
  let originFlag = originOrder.split(':')[0];
  let originSort = originOrder.split(':')[1];
  var newOrder = '';
  
  if(flag == originFlag){ // 如果相等就判断排序的值(注意取反,是desc取反asc)
    newOrder = `${originFlag}:${originSort == "desc"?"asc":"desc"}`
  }else{
    newOrder = `${flag}:${originSort == "desc"?"asc":"desc"}` //同样的套路
  }
  // 更新值并发请求
  this.searchParams.order = newOrder;
  this.getData();
}

  • 排序思路小结
- 先搞定背景色高亮样式(点击哪个字段,那个字段就高亮)
- 再搞定'箭头'图标(点击一次字段,箭头反向)
- 最后根据"数据格式"搞定排序(order:"1:asc" || order:"1:desc")

分页器(Search组件需引入)

  • 先注册为全局组件,然后搞定静态结构
### main.js
......
import Pagination from '@/components/Pagination'
Vue.component(Pagination.name,Pagination)

### Pagination.index.vue
<template>
	<div class="pagination">
		<button>1</button>
		<button>上一页</button>
		<button>•••</button>

		<button>3</button>
		<button>4</button>
		<button>5</button>
		<button>6</button>
		<button>7</button>

		<button>•••</button>
		<button>9</button>
		<button>上一页</button>

		<button style="margin-left: 30px">共 60 条</button>
	</div>
</template>
<script>
	export default {
		name: "Pagination"
	}
</script>

<style lang="less" scoped>
	.pagination {
		text-align: center;

		button {
			margin: 0 5px;
			background-color: #f4f4f5;
			color: #606266;
			outline: none;
			border-radius: 2px;
			padding: 0 4px;
			vertical-align: top;
			display: inline-block;
			font-size: 13px;
			min-width: 35.5px;
			height: 28px;
			line-height: 28px;
			cursor: pointer;
			box-sizing: border-box;
			text-align: center;
			border: 0;

			&[disabled] {
				color: #c0c4cc;
				cursor: not-allowed;
			}

			&.active {
				cursor: not-allowed;
				background-color: #409eff;
				color: #fff;
			}
		}
	}
</style>

### Search.index.vue
......
<div class="goods-list">
	......
<Pagination/>
  • 了解分页器必须的几个字段
- 当前页数: pageNo

- 每页展示多少条数据: pageSize

- 整个分页器一共有多少条数据(通过计算得出一共有多少页): total

- 分页器连续页码的个数: 5|7[奇书],奇数对称好看, continues

- 举例: 每一页3条数据,一共91条数据[一共是31页]
  • 先使用假数据传参
### Search.index.vue
......
<!--传入4个假数据-->
<Pagination :pageNo="31" :pageSize="3" :total="91" :continues="5"/>
  • Pagination.index.vue接收参数并计算
### Pagination.index.vue
<template>
	<div class="pagination">
		<button>上一页</button>
		<button v-if="startNumAndEndNum.start > 1">1</button> <!--显示页码1-->
		<button v-if="startNumAndEndNum.start > 2">•••</button> <!--显示"..."-->
		<!--中间部分-->
		<button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start">{{page}}</button>
		
		<!--结尾-->
		<button v-if="startNumAndEndNum.end < totalPage - 1">•••</button>
		<button v-if="startNumAndEndNum.end < totalPage">{{totalPage}}</button>
		<button>下一页</button>

		<button style="margin-left: 30px">共 {{total}} 条</button>
	</div>
</template>
<script>
	export default {
		name: "Pagination",
		props:["pageNo","pageSize","total","continues"], // 接收分页器参数
		computed:{
			totalPage(){ // 计算总页数
				// 向上取整:比如结果为35.5,那么最终结果就是36
				return Math.ceil(this.total/this.pageSize)
			},
			startNumAndEndNum(){ // 计算起始页码数和结尾页码数
				const {continues,pageNo,totalPage} = this; // 这个解构赋值看不懂...
				let start = 0,end = 0; // 初始化值
				// 不够5页
				if(continues > totalPage){
					start=1;
					end=totalPage;
				}else{
					start = pageNo - parseInt(continues / 2)
					end = pageNo + parseInt(continues / 2)
					// 开头页面和结尾页码纠正
					if(start < 1){
						start = 1;
						end = continues;
					}
					if(end > totalPage) {
						end = totalPage;
						start = totalPage - continues + 1;
					}
				}
				return {start,end}
			}
		}
	}
</script>

<style lang="less" scoped>
	......
</style>

页码数据的动态渲染(之前是用写死的数据作开发测试用)

### Search.index.vue
// 之前写死的数据
<Pagination :pageNo="1" :pageSize="3" :total="91" :continues="5"/>

- pageNo和pageSize参数在searchParams中已有,total参数在仓库里面,现在获取

### Search.index.vue

computed: {
  // 获取total参数值
  ...mapState({total:state=>state.search.searchList.total})
},

// 渲染 Pagination(continues自己手动决定,模仿大厂选择5)
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" />

  • 还得获取Pagination组件传过来的页码数,涉及到子传父通讯,所以再给父组件Search绑定自定义事件
<Pagination ...... @getPageNo="getPageNo"/> // 绑定自定义事件
......
getPageNo(pageNo){
  this.searchParams.pageNo = pageNo; // 重新赋值并再次发请求
  this.getData();
}
  • Pagination组件把用户点击的页码传给父组件Search,并点亮页码高亮背景样式
### Pagination.index.vue
<template>
	<div class="pagination">
		<!--第一页时,按钮设置成'不可用';触发事件并传参-->
		<button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button>
		<!--触发事件并传参;设置active样式-->
		<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)" :class="{active:pageNo==1}">1</button>
		......
		<!--触发事件并传参;设置active样式-->
		<button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo',page)"  :class="{active:pageNo==page}">{{page}}</button>

		......
		<!--触发事件并传参;设置active样式-->
		<button v-if="startNumAndEndNum.end < totalPage"  @click="$emit('getPageNo',totalPage)"  :class="{active:pageNo==totalPage}">{{totalPage}}</button>
		<button @click="$emit('getPageNo',pageNo+1)">下一页</button>

		......
	</div>
</template>
......
<style>
......
.active { // 设置背景色
		background-color: skyblue;
	}
</style>

Detail组件开发

  • 先搞定静态组件(教程已备)
  • 搞定路由
### router.index.js
......
export default new VueRouter({
	routes:[
		......
		{
			path:"/detail/:skuid", // 必须传skuid表明访问的是哪一个sku(不传skuid是无法到达Detail组件的)
			component:Detail,
			meta:{show:true}
		},
		......
	]
})

- 测试可否正常访问: http://localhost:8080/#/detail/27
  • 搞定 Search组件中,商品列表的url图片链接,实现对sku的静态页面访问
### Search.index.vue
......
<div class="goods-list">
  ......
    <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id">
      ......
      	  <!--注意to值的js语法`string/${xxx}`-->
          <router-link :to="`/detail/${good.id}`"> <!--把原来的<a>替换成 <router-link>-->
            <img :src="good.defaultImg" />
          </router-link>
       ......
  • router模块化分工优化一下代码
### 新建router.routes.js
import Home from '@/pages/Home'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
import Search from '@/pages/Search'
import Detail from '@/pages/Detail'

export default [
	{ // 项目跑起来,跳转到home页
		path: "*",
		redirect: "/home"
	},
	{
		path: "/detail/:skuid",
		component: Detail,
		meta: {
			show: true
		}
	},
	{
		path: "/home",
		component: Home,
		meta: {
			show: true
		}
	},
	{
		name: "search",
		path: "/search",
		component: Search,
		meta: {
			show: true
		}
	},
	{
		path: "/login",
		component: Login
	},
	{
		path: "/register",
		component: Register
	},
]

### router.index.js
......
import routes from './routes' // 导入
......
export default new VueRouter({
	routes,
})

  • 滚动bug修复: 此时当我们点进detail页面的时候,页面不会位于最上方,滚动条会被拖动一段,现在解决它

    vue的官方文档,就有关于此类问题的具体解决办法,有兴趣可以去看看

### router.index.js
......
export default new VueRouter({
	routes,
	scrollBehavior(to,from,savedPosition) { // 加入这个配置项
		return {y:0}; // 滚动条位于页面顶部
	}
})
  • 发请求
### api.index.js
......
export const reqGoodsInfo = (skuId)=>requests({ //带着skuId发请求
	url:`/item/${skuId}`, // 注意这种语法格式
	method:'get'
})
  • vuex三连环
### 新建 store.detail.index.js

import {reqGoodsInfo} from "@/api" // 导入请求对象

const actions = {
	async getGoodsInfo({commit},skuId){ // 发请求
		var res = await reqGoodsInfo(skuId)
		if(res.code==200){
			commit('GETGOODSINFO',res.data)
		}
	}
}
const mutations = {
	GETGOODSINFO(state,goodsInfo){ // 加工数据
		state.goodsInfo = goodsInfo
	}
}
const state = {
	goodsInfo:{} // 初始化并稍后存储
}
const getters = { // 简化代码
	categoryView(state){
		return state.goodsInfo.categoryView || {}
	},
	skuInfo(state){
		return state.goodsInfo.skuInfo || {}
	}
}

export default {
	actions,
	mutations,
	state,
	getters
}

### store.index.js
......
import detail from './detail/index.js'
......
export default new Vuex.Store({
  modules: {
	  ......
	  detail // 新注册
  }
})

  • 数据的渲染
### Detail.index.vue
......
<section class="con">
  <div class="conPoin">
  	<!--面包屑数据的渲染-->
    <span v-show="categoryView.category1Name">{{categoryView.category1Name}}</span>
    <span v-show="categoryView.category2Name">{{categoryView.category2Name}}</span>
    <span v-show="categoryView.category3Name">{{categoryView.category3Name}}</span>
  </div>
  ......
    <div class="InfoWrap">
      ......
      	<!--商品的名字,描述和价格 渲染-->
        <h3 class="InfoName">{{skuInfo.skuName}}</h3>
        <p class="news">{{skuInfo.skuDesc}}</p>
            ......
              <i>¥</i>
              <em>{{skuInfo.price}}</em>
              <span>降价通知</span>
 .......
 <script>
  ......
  import {mapGetters} from 'vuex'

  export default {
    name: 'Detail',
    components: {
     ......
    },
	computed:{ // 映射那两个简化的数据
		...mapGetters(['categoryView','skuInfo'])
	},
	mounted(){
		// 组件挂载完毕就派发请求
		this.$store.dispatch('getGoodsInfo',this.$route.params.skuId)
	}
  }
</script>

放大图小图片数据的渲染

### detail.index.vue
......
<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>
<!-- 小图列表 -->
<ImageList :skuImageList="skuImageList"/>
......
<script>
 ......

  export default {
    name: 'Detail',
    components: {
      ImageList,
      Zoom
    },
	computed:{
		...mapGetters(['categoryView','skuInfo']),
		skuImageList(){
			return this.skuInfo.skuImageList || [] // 网络请求正常的情况下当然没问题(万一呢...)
		}
		
	},
	mounted(){
		......
	}
  }
</script>

### Zoom.vue
<template>
  <div class="spec-preview">
    <img :src="imageObj.imgUrl" /> <!--大图和小图一样的数据,只不过通过css控制图片大小-->
    ......
    <div class="big">
      <img :src="imageObj.imgUrl" />
    </div>
    ......
</template>

<script>
  export default {
    name: "Zoom",
	props:["skuImageList"],
	computed:{
		imageObj(){
			return this.skuImageList[0] || {} // 返回空对象,避免控制台警告
		}
	}
  }
</script>

### ImageList.vue
<template>
  <div class="swiper-container">
    ......
      <div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="slide.id">
        <img :src="slide.imgUrl"> <!--渲染小图-->
      </div>
   ......
  </div>
</template>

<script>

  import Swiper from 'swiper'
  export default {
    name: "ImageList",
	props:["skuImageList"]
  }
</script>

售卖属性数据的渲染

### store.detail.index.js 
......
const getters = {
	......
	// 简化售卖属性的数据
	spuSaleAttrList(state){
		return state.goodsInfo.spuSaleAttrList || []
	}
}

### detail.index.vue
......
 <div class="choose">
  <div class="chooseArea">
    <div class="choosed"></div>
    <!--渲染数据-->
    <dl v-for="(spuSaleAttr,index) in spuSaleAttrList" :key="spuSaleAttr.id">
      <dt class="title">{{spuSaleAttr.saleAttrName}}</dt>
      					<!--样式是否激活,由后端数据isChecked来决定-->
      <dd changepirce="0" :class="{active:spuSaleAttrValue.isChecked == 1}" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" 
      :key="spuSaleAttrValue.id">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
    </dl>
  </div>
  <div class="cartWrap">
......
computed:{
  ...mapGetters(['categoryView','skuInfo','spuSaleAttrList']), // 映射数据
},

售卖属性排他操作

  • 比如用户点击'红色','16G',这两个属性应该有高亮效果,而其他属性则没有高亮效果
- 项目中,'属性值'是否高亮,是由'spuSaleAttrValue.isChecked == 1'决定
- 所以,当用户点击'属性值'时,该'属性值'的'isChecked'必须设置为'1',而其他属性值必须不为1,这就是'排他'操作
### detail.index.vue
......
<div class="chooseArea">
  ......
  <dl v-for="(spuSaleAttr,index) in spuSaleAttrList" :key="spuSaleAttr.id">
    <dt class="title">{{spuSaleAttr.saleAttrName}}</dt>
    <dd changepirce="0" :class="{active:spuSaleAttrValue.isChecked == 1}" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" 
:key="spuSaleAttrValue.id"
<!--绑定点击事件并传入'用户点击的项'和'项列表'-->
@click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
......
methods:{
  changeActive(spuSaleAttrValue,spuSaleAttrValueList){
    spuSaleAttrValueList.forEach(item=>{ // 遍历所有项,先统一设置
      item.isChecked = 0;
    })
    spuSaleAttrValue.isChecked = 1; // 筛选用户点击的项并设置值
  }
}

ImageList组件轮播图操作

  • 参考全局轮播图组件carousel的写法(Question: 详情页的轮播图是否可以使用carousel)
### ImageList.ImageList.vue
......
<template>
  <div class="swiper-container" ref="cur"> <!--新增ref标识-->
    ......
  </div>
</template>

<script>
  import Swiper from 'swiper'
  
  export default {
    name: "ImageList",
    props:["skuImageList"],
    watch:{
    // 监视属性 + $nextTick 保证轮播图数据结构完整
    skuImageList(newVal,oldVal){
      this.$nextTick(()=>{
        new Swiper(this.$refs.cur, {
          // 前进后退按钮
          navigation: {
            nextEl: '.swiper-button-next',
            prevEl: '.swiper-button-prev',
          },
          // 显示几个图片设置
          slidesPerView:3,
          // 每一次切换图片的个数
          slidesPerGroup:1
        })
      })
    }
  }
  }
</script>

  • 轮播图高亮效果的展示: 初始化的轮播图高亮效果,是使用css实现,我们模仿之前的套路,利用索引,使用js来实现
### ImageList.ImageList.vue
......
<style lang="less" scoped>
  .swiper-container {
        ......
        // &:hover { // 注释掉,使用js来实现
        //   border: 2px solid #f60;
        //   padding: 1px;
        // }
 ......
</style>
### ImageList.ImageList.vue

<template>
  ......
  <div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="slide.id">
    <img :src="slide.imgUrl" @click="changeCurrentIndex(index)" :class="{active:currentIndex == index}"> <!--当两个值相等时,就激活样式-->
  ......
</template>


<script>
  import Swiper from 'swiper'
  
  export default {
    ......
    data(){
    return {
      currentIndex:0 // 初始值
    }
  },
  methods:{
    changeCurrentIndex(index){
      this.currentIndex = index // 用户点击就赋值相同
    }
  },
  ......

<style lang="less" scoped>
  ......
  &.active { // 提前写好的样式
    border: 2px solid #f60;
    padding: 1px;
  }
  ......
</style>
  • 当用户点击轮播图的时候,Zoom组件里面的大图也应该展示对应的轮播图(目前是写死的数据)
- 由于 Zoom组件 和 ImageList组件 渲染的是同一数据,所以需要 ImageList组件把'索引'传给 Zoom组件
- 涉及兄弟组件之间的传值,我们使用'$bus'来实现
### ImageList.vue
......
methods:{
  changeCurrentIndex(index){
    this.currentIndex = index
    this.$bus.$emit('getIndex',this.currentIndex) // 绑定$bus并传索引
  }
},

### Zoom.vue
......
<script>
  export default {
    ......
    data(){
    return {
      currentIndex:0 // 初始值
    }
  },
  computed:{
    imageObj(){
      return this.skuImageList[this.currentIndex] || {} // 不再是之前写死的数据
    }
  },
  mounted(){
    this.$bus.$on('getIndex',(index)=>{ // 接收并赋值
      this.currentIndex = index;
    })
  }
  }
</script>

放大镜效果

  • 照抄代码吧,暂时看不懂(需结合css样式进行计算)
### Zoom.vue

<template>
  <div class="spec-preview">
    <img :src="imageObj.imgUrl" />
    <!--绑定鼠标移动事件-->
    <div class="event" @mousemove="handler"></div>
    <div class="big">
      <img :src="imageObj.imgUrl" ref="big"/> <!--增加ref标识-->
    </div>
  <!--遮罩层-->
    <div class="mask" ref="mask"></div> <!--增加ref标识-->
  </div>
</template>

<script>
  ......
  },
  methods:{
    handler(event){
      let mask = this.$refs.mask;
      let big = this.$refs.big;
      let left = event.offsetX - mask.offsetWidth/2;
      let top = event.offsetY - mask.offsetHeight/2;
      // 约束范围
      if(left <= 0) left = 0;
      if(left >= mask.offsetWidth) left = mask.offsetWidth;
      if(top <= 0) top = 0;
      if(top >= mask.offsetHeight) top = mask.offsetHeight;
      // 修改元素的left|top属性值
      mask.style.left = left + 'px';
      mask.style.top = top + 'px';
      big.style.left = -2 * left + 'px';
      big.style.top = -2 * top + 'px';
    }
  },
  computed:{
    .......
  },
  mounted(){
    ......
  }
  }
</script>

<style lang="less">
  .spec-preview {
    position: relative;
    width: 400px;
    height: 400px;
    border: 1px solid #ccc;

    img {
      width: 100%;
      height: 100%;
    }

    .event {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 998;
    }

    .mask {
      width: 50%;
      height: 50%;
      background-color: rgba(0, 255, 0, 0.3);
      position: absolute;
      left: 0;
      top: 0;
      display: none;
    }

    .big {
      width: 100%;
      height: 100%;
      position: absolute;
      top: -1px;
      left: 100%;
      border: 1px solid #aaa;
      overflow: hidden;
      z-index: 998;
      display: none;
      background: white;

      img {
        width: 200%;
        max-width: 200%;
        height: 200%;
        position: absolute;
        left: 0;
        top: 0;
      }
    }

    .event:hover~.mask,
    .event:hover~.big {
      display: block;
    }
  }
</style>

加入购物车的+,-input框的处理

- '+'号可以一直加,没有问题
- '-'号减到input框值为'1'的时候,就不能再减
- 'input框'值允许用户随意输入,但是js必须对值进行转换
	- 不合理的值,一律转换为'1'
### detail.index.vue
......
<div class="cartWrap">
  <div class="controls">
  	<!--绑定chang事件,当input框失去焦点的时候,就是用户的最终输入值-->
    <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum">
    <a href="javascript:" class="plus" @click="skuNum++">+</a> <!--自加无限制-->
    <a href="javascript:" class="mins" @click="skuNum>1?skuNum--:skuNum=1">-</a> <!--自减作限制-->
  </div>
  <div class="add">
    <a href="javascript:">加入购物车</a>
  </div>
</div>
......
<script>
  ......
  data(){
    return {
      skuNum:1 // 初始化数据
    }
  },
  ......
  methods:{
    ......
    changeSkuNum(event){
      var value = event.target.value*1 // 当用户输入'字符串',结果就是 NaN
      if(isNaN(value)||value<1){
        this.skuNum = 1; // 输入不合理,就转换为1
      }else{
        this.skuNum = parseInt(value) // 判断用户输入小数,就向下取整
      }
    }

</script>

用户点击加入购物车按钮,向后端发送请求

- 文档接口
	- 地址: /api/cart/addToCart/{ skuId }/{ skuNum }
	- 请求方式: post
	- 成功示例:
		{
            "code": 200,
            "message": "成功",
            "data": null, // 响应成功,并没有data,这是正常的,由后端去决定
            "ok": true
        }
  • 配置请求
### api.index.js
......
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({
	url:`/cart/addToCart/${skuId}/${skuNum}`,
	method:'post'
})

### main.js 中测试一下请求是否正常
import {reqAddOrUpdateShopCart} from './api'
var res = reqAddOrUpdateShopCart(1,1)
console.log(res)
  • 仓库发请求
### store.detail.js
......
import {reqAddOrUpdateShopCart} from "@/api"

const actions = {
	......
	async addOrUpdateShopCart({commit},{skuId,skuNum}){ // 解构赋值
		var res = await reqAddOrUpdateShopCart(skuId,skuNum)
		if(res.code==200){
			return 'ok' // 由于data为空,这里自定义返回值
		}else{
			return Promise.reject(new Error('fail')) // 返回Promise异常
		}
	}
	
}
  • vue模板派发请求
### detail.vue
......
<div class="cartWrap">
 ......
  <div class="add">
    <a href="javascript:" @click="addShopCart">加入购物车</a> <!--绑定点击事件-->
  </div>
</div>
......
methods:{
  ......
  async addShopCart(){ // 声明异步函数
    try{
      await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuId,skuNum:this.skuNum})
    }catch(error){
      alert(error.message) // 请求异常下,会输出Promise对象异常信息
    }   
  }
}
  • 依据业务需求,除了派发请求加入购物车以后,还需要跳转到添加购物车成功页面
- 这个跳转无需向后端交互,意味着sku数据需要前端从当前页面转移过去
	- 大的数据,比如 skuInfo 就放到浏览器的 sessionStorage
	- 小的数据,比如 skuNum(用户购买的数量),我们放到 router.query中
......
async addShopCart(){
  try{
    await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuId,skuNum:this.skuNum});
    sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo)) // 存到 sessionStorage,注意序列化操作
    this.$router.push({name:"addcartsuccess",query:{skuNum:this.skuNum}}) // query传参
  }catch(error){
    alert(error.message)
  }   
}
  • 添加AddCartSuccess静态组件
### AddCartSuccess.index.vue

<template>
  <div class="cart-complete-wrap">
      ......
            <img :src="skuInfo.skuDefaultImg"> <!--渲染购物车数据-->
          </div>
          <div class="right-info">
            <p class="title">{{skuInfo.skuName}}</p><!--渲染购物车数据-->
            <p class="attr">{{skuInfo.skuDesc}} 数量: {{skuNum}}</p>
          </div>
        </div>
        <div class="right-gocart">
          <!--渲染上一级页面链接-->
		  <router-link :to="`/detail/${skuInfo.id}`" class="sui-btn btn-xlarge">查看商品详情</router-link>
		  <!--渲染ShopCart组件页面链接-->
          <router-link to="/shopcart">去购物车结算</router-link>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'AddCartSuccess',
	computed:{
		skuInfo(){ // 反序列化 sessionStorage 中存储的数据
			return JSON.parse(sessionStorage.getItem('SKUINFO'))
		},
		skuNum(){
			return this.$route.query.skuNum // 简化代码
		}
	}
  }
</script>

<style lang="less" scoped>
 ......
</style>

ShopCart组件: 先生成静态页面ShopCart.index.vue

获取购物车数据

  • 发请求获取购物车数据
### ShopCart.index.vue
......
<script>
  export default {
    name: 'ShopCart',
	methods:{
		getData(){ // 封装
			this.$store.dispatch('getCartList')
		}
	},
	mounted(){
		this.getData() // 派发
	}
  }
</script>

### api.index.js
......
export const reqCartList = ()=>requests({ // 无需携带任何参数
	url:"/cart/cartList", // 这里注意'List'不能写成小写的'list',会出现费解的问题(会向本地服务器发请求,而不是目标服务器...)
	method:'get'
})

### 新增 store.shopcart.index.js
import { reqCartList } from "@/api" // 导入

const actions = {
	async getCartList({commit}){ // 发请求测试
		var res = await reqCartList()
		console.log(res)
	}
}
......
  • 结果是有问题的,data为空,所以木有办法渲染数据(服务器需要身份标识)
{code: 200, message: '成功', data: Array(0), ok: true}
  • 解决思路
- 我们通过'uuid'为匿名用户生成唯一的'身份标识',加入headers发给后台即可
### 新增 utils.uuid_token.js
import { v4 as uuidv4} from 'uuid'

export const getUUID = ()=>{ // 判断本地存储是否有token,再决定是否生成,最终返回token
	let uuid_token = localStorage.getItem('UUIDTOKEN');
	if(!uuid_token){
		uuid_token = uuidv4();
		localStorage.setItem('UUIDTOKEN',uuid_token)
	}
	return uuid_token;
}
- 生成的'uuid'要存在哪里?需要的地方是'加入购物车'接口,所以把它放到'detail'的仓库

### store.detail.index.js
......
import {getUUID} from '@/utils/uuid_token.js' // 导入

const state = {
	......
	uuid_token:getUUID() // 获取 uuid
}

- 利用vue的谷歌插件,查看此时的仓库,是否有uuid

- 加入购物车的接口,并没有要求带 uuid 参数,只要求'skuId'和'skuNum',要如何发给后端?答案是放到'headers(与后端约定好名称)
	export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({
        ......
    })
    
- 在'请求拦截器'中,实现headers
### api.request.js
......
import store from '@/store/index.js' // 导入store
......
requests.interceptors.request.use((config)=>{
	if(store.state.detail.uuid_token){ // 判断uuid_token
		// userTempId 是和后端约定好名称
		config.headers.userTempId = store.state.detail.uuid_token
	}
	nprogress.start();
	return config // config里面有一个headers请求头,是重要参数
})

- 响应成功结果:data中有数据了
	{code: 200, message: '成功', data: Array(1), ok: true}


  • 仓库数据的处理
### store.shopcart.index.js

import { reqCartList } from "@/api"

const state = {
	cartList:[] // 初始化
}

const actions = {
	async getCartList({commit}){
		var res = await reqCartList()
		if(res.code == 200){
			commit('GETCARTLIST',res.data) // 提交数据
		}
	}
}
const mutations = {
	GETCARTLIST(state,cartList){ // 加工
		state.cartList = cartList
	}
}

const getters = {
	cartList(state){
		return state.cartList[0] || {} // 进一步简化代码
	}
}

export default {
	actions,
	mutations,
	state,
	getters
}

  • 数据的渲染(包含'小计算')
### shopCart.index.vue
......
<ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id"><!--遍历每一项-->
  <li class="cart-list-con1">
    <input type="checkbox" name="chk_list" :checked="cart.isChecked==1"><!--是否被勾选-->
  </li>
  <li class="cart-list-con2">
    <img :src="cart.imgUrl"> <!--图片和商品名称-->
    <div class="item-msg">{{cart.skuName}}</div>
  </li>
  <li class="cart-list-con3">
    <div class="item-txt">语音升级款</div>
  </li>
  <li class="cart-list-con4">
    <span class="price">{{cart.skuPrice}}</span> <!--单价-->
  </li>
  <li class="cart-list-con5">
    <a href="javascript:void(0)" class="mins">-</a> <!--更改商品数量涉及到发请求,暂不处理-->
    <input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" >
    <a href="javascript:void(0)" class="plus"  >+</a>
  </li>
  <li class="cart-list-con6">
    <span class="sum">{{cart.skuPrice*cart.skuNum}}</span> <!--计算单价总和-->
  </li>
  <li class="cart-list-con7">
    <a href="#none" class="sindelet">删除</a>
    <br>
    <a href="#none">移到收藏</a>
  </li>
</ul>
......
 <div class="sumprice">
    <em>总价(不含运费) :</em>
    <i class="summoney">{{totalPrice}}</i> <!--所有商品总价-->
  </div>
......
<script>
  import {mapGetters} from 'vuex';
  
  export default {
    name: 'ShopCart',
 ......
  computed:{
    ...mapGetters(['cartList']), // 映射仓库数据
    cartInfoList(){
      return this.cartList.cartInfoList || [] // 进一步简化
    },
    // 计算所有产品的总价
    totalPrice(){
      var sum = 0;
      this.cartInfoList.forEach((item)=>{
        sum += item.skuPrice*item.skuNum
      });
      return sum
    },
    // 判断是否全选
    isAllCheck(){
      // return this.cartInfoList.every((item)=>{
      //  return this.cartInfoList.isChecked == 1
      // })
      // every会遍历每一项,对函数体的条件进行判断,返回true || false
      return this.cartInfoList.every(item=>this.cartInfoList.isChecked == 1)
    }
  },
  ......

</script>
  • 修改商品数量逻辑分析
- 我们需要三个参数

	- type: 判断是哪一个元素
	- disNum: 增量/减量
	- cart: 商品对象

### shopCart.index.vue
......
<li class="cart-list-con5">
  <!--演示代码-->
  <a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a>
  <input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value*1,cart)"> <!--$event.target.value*1判断用户是否输入'数字'-->
  <a href="javascript:void(0)" class="plus"  @click="handler('add',1,cart)">+</a>
</li>
......
methods:{
  ......
  handler(type,disNum,cart){
    console.log(type,disNum,cart) // 测试
  }
},

修改购物车商品数量

### ShopCart.index.vue
......
<!--结构-->
<li class="cart-list-con5">
  <a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a>
  <input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt"
    @change="handler('change',$event.target.value*1,cart)">
  <a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a>
</li>
......
<script>
  ......
  import {
    throttle
  } from 'lodash';

  export default {
    name: 'ShopCart',
    methods: {
      getData() {
        ......
      },
      // 这里要做'节流'
      handler: throttle(async function(type, disNum, cart) {
        switch (type) {
          case "add":
            disNum = 1;
            break
          case 'minus':
            // :value="cart.skuNum"
            disNum = cart.skuNum > 1 ? -1 : 0
            break
          case 'change':
            disNum = isNaN(disNum) || disNum < 1 ? 0 : parseInt(disNum) - cart.skuNum
            break
        }
        try {
          await this.$store.dispatch('addOrUpdateShopCart', {
            skuId: cart.skuId,
            skuNum: disNum
          });
          this.getData();
        } catch (error) {
          alert(error.message)
        }
      }, 600)
    },
    ......
  }
</script>

删除购物车

### api.index.js
......
// 删除购物车产品
// 配置请求(只是通知服务器的动作,返回data为null)
export const reqDeleteCartById = (skuId) => requests({url: `/cart/deleteCart/${skuId}`, method: 'delete'})

- vuex连环
### store.shopcart.index.js
......
const actions = {
	......
	async deleteCartListBySkuId({commit},skuId){ // data为null的情况,就这么搞
		var res = await reqDeleteCartById(skuId);
		if(res.code==200){
			return 'ok'
		}else{
			return Promise.reject(new Error('fail'))
		}
	}
}

### ShopCart.index.vue
......
<li class="cart-list-con7">
	<!--绑定点击事件.传参-->
    <a @click="deleteCartById(cart)" class="sindelet">删除</a>
    <br>
    <a href="#none">移到收藏</a>
</li>
......
deleteCartById:throttle(async function(cart){ // 节流+async的写法
  try{
    await this.$store.dispatch('deleteCartListBySkuId',cart.skuId)
    this.getData();
  }catch(error){
    alert(error.message)
  }
},1000) 

商品的勾选处理

### api.index.js
......
// 切换商品选中状态
export const reqUpdateCheckedByid = (skuId, isChecked) => requests({
    url: `/cart/checkCart/${skuId}/${isChecked}`,
    method: 'get'
})

### store.shopcart.index.js
......
const actions = {
  ......
  // 修改购物车商品选中状态
  async updateCheckedById({commit}, {skuId, isChecked}) {
      let result = await reqUpdateCheckedByid(skuId, isChecked);
      if (result.code == 200) {
          return 'ok'
      } else {
          return Promise.reject(new Error('faile'));
      }
  },
}
### ShopCart.index.vue
......
<!--结构-->
<ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id">
  <li class="cart-list-con1">
    <input type="checkbox" name="chk_list" :checked="cart.isChecked==1" @change="updateChecked(cart,$event)"> <!--处理单个商品勾选/不勾选-->
  </li>
......
async updateChecked(cart,event){
  try{
    let isChecked = event.target.checked ? '1' : '0' // 获取单个商品是否勾线
    await this.$store.dispatch('updateCheckedById',{ // 发请求更新
      skuId:cart.skuId,
      isChecked
    });
    this.getData() // 重新渲染最新数据
  }catch(error){
    alert(error.message)
  }
}

删除被选中的商品

### ShopCart.index.vue
......
<div class="option">
  <!--绑定事件(这里无法传ID,因为结构不同,传不到这里)-->
  <a @click="deleteAllCheckedCart">删除选中的商品</a>
  ......
</div>
......
async deleteAllCheckedCart(){
        try{
                await this.$store.dispatch('deleteAllCheckedCart') //派发新请求
                this.getData()
        }catch(error){
                alert(error.message)
        }

}
### store.shopcart.index.js
......
deleteAllCheckedCart({getters,dispatch}){
		// console.log(context)
		let promiseAll = []
		getters.cartList.cartInfoList.forEach(item=>{
			// 成功就返回 Promise对象
			let promise = item.isChecked==1 ? dispatch('deleteCartListBySkuId',item.id) : ''
			promiseAll.push(promise) // 收集
		})
		return Promise.all(promiseAll) // 要么一起成功,要么一起失败
	}

全勾全不勾

- '全选'框绑定change事件

### ShopCart.index.vue
<div class="select-all">
		<!--绑定updateAllCartChecked-->
		<!--cartInfoList.length>0 当购物车商品被删光以后,不能再有'勾选'的状态-->
        <input class="chooseAll" type="checkbox" :checked="isAllChec k&& cartInfoList.length>0" @change="updateAllCartChecked">
        <span>全选</span>
</div>
......
async updateAllCartChecked(event){
        try{
                let isChecked = event.target.checked? 1 : 0 // 获取target元素的状态
                await this.$store.dispatch('updateAllCartIsChecked',isChecked) // 派发请求并传参
                this.getData();// 重新刷新数据
        }catch(error){
                alert(error.message)
        }
        
}

### store.shopcart.index.js
......
updateAllCartIsChecked({getters,dispatch},isChecked){
        let promiseAll = []
        getters.cartList.cartInfoList.forEach(item=>{
                let promise = dispatch('updateCheckedById',{ // 调用单勾商品的函数
                        skuId:item.id,
                        isChecked
                })
                promiseAll.push(promise)
        })
        return Promise.all(promiseAll) // 最终返回true || false
}