排序
的操作
- 数据格式说明
- '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>
### 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.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'
})
### 新建 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框值为'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异常
}
}
}
### 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.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
}