尚硅谷VUE项目-前端项目问题总结07--产品详情页【vuex-排他操作foreach-放大镜-轮播图-兄弟组件通信$bus-购物车-路由跳转传参-路由传参+会话存储】-游客身份-节流


四个步骤:
1.静态组件(详情页还未注册为路由组件)
2.发请求
3.vuex
4.动态展示组件

1.静态组件(详情页还未注册为路由组件)

父组件index,两个子组件【放大镜和小图】
点击商品时,跳转到详情页,路由跳转需要传参【产品id】给详情页
在router-index中引入和添加

import Detail from '@/pages/Detail'

 {
            path: '/detail/:skuid',//params传参,:占位
            component: Detail,
            meta: {
                show: true,
            },
        },


 <router-link :to="`/detail/${item.id}`">
       <img :src="item.defaultImg" />
  </router-link>

知识点:
1.把router-》index中routes数组摘出来放在routes.js中统一管理,然后再引入即可
routes.js中:

//引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
import Detail from '@/pages/Detail'


//路由配置信息
export default [
    //重定向
    {
        path: '*',
        redirect: '/home',
    },
    {
        path: '/home',
        component: Home,
        meta: {
            show: true,
        },
    },
    {
        path: '/detail/:skuid',//params传参,:占位
        component: Detail,
        meta: {
            show: true,
        },
    },
    {
        path: '/search/:keyword?',//:params传参的占位;?参数可传可不传
        component: Search,
        meta: {
            show: true,
        },
        name: 'search',//传参方式为对象,用params传参
        // props:true, //路由组件传props数据  第一种方法:布尔值写法-只能是params

        // props:{a:1,b:2}, //路由组件传props数据  第二种方法:对象   额外的给路由组件传递一些props

        // props:($route)=>{//路由组件传props数据  第三种方法:函数  可以把params和query参数,通过props传递给路由组件
        //     return {keyword:$route.params.keyword,joan:$route.query.joan}
        // }, 
        // 简写
        props: ($route) => ({ keyword: $route.params.keyword, joan: $route.query.joan }),
    },
    {
        path: '/login',
        component: Login,
        meta: {
            show: false,
        },
    },
    {
        path: '/register',
        component: Register,
        meta: {
            show: false,
        },
    },
]

router-》index.js

import routes from './routes'

//配置路由
export default new VueRouter({
    //配置路由
    // routes: routes
    routes

})

2.点开详情,定位到最顶部
路由解决滚动行为
vue官网=》生态系统=》Vue Router =.>滚动行为

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    // 始终滚动到顶部
    return { top: 0 } //vue3
    return {y:0}      //vue2
  },
})
export default new VueRouter({
    //配置路由
    // routes: routes
    routes,
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        // return { top: 0 } //vue3
        return { y: 0 }      //vue2
    },
    //限制单个路由可以通过to属性来判断:if to.name == detail 在返回

})

2.发请求

//获取商品详情  /api/item/{ skuId }    get
export const reqGoodsInfo=(skuId)=>requests({url:`/item/${ skuId }`,method:'get'})

3.vuex-获取产品详情信息

重新建一个store-》details.js文件存储信息【仍然是四件套】

const state={};
const mutations={};
const actions={};
const getters={};
//对外暴露一个对象
export default{
    state,
    mutations,
    actions,
    getters,
}

合并到index中:引入,注册

import detail from './detail'

export default new Vuex.Store({
    //没有模块式开发,都写在这
    // state,
    // mutations,
    // actions,
    // getters,
    // modules,

    //实现Vuex仓库【模式开发】存储数据
    //模块:把小仓库进行合并变为大仓库
    modules:{
        home,
        search,
        detail,
    }

})

在actions中发请求:接口reqGoodsInfo

import {reqGoodsInfo} from '@/api'
const state={
    //不要乱写,看接口文档返回数据类型,是{}
    goodInfo:{},
};
const mutations={
    GETGOODINFO(state,goodInfo){
        state.goodInfo=goodInfo;
    }
};
const actions={
    //获取产品的action
    async getGoodInfo({commit},skuid){
        let result=await reqGoodsInfo(skuid)
        if(result.code==200){
            commit('GETGOODINFO',result.data)
        }
    },
};
const getters={};
//对外暴露一个对象
export default{
    state,
    mutations,
    actions,
    getters,
}

派发actions

mounted(){
      //点击产品进入详情页,路由跳转时携带skuId
     this.$store.dispatch('getGoodInfo',this.$route.params.skuid)
    },

因为goodInfo数据复杂,所以在getters计算后返回

const getters={
    categoryView(state){
        return  state.goodInfo.categoryView||{}
    },
    skuInfo(state){
        return  state.goodInfo.skuInfo||{}
    },
    spuSaleAttrList(state){
        return  state.goodInfo.spuSaleAttrList||[]
    },
};

子组件获取数据

 computed:{
       //没用命名空间用下方的方式,[如果开启了命名空间的这样拿  ...mapGetters('search',['goodsList'])]
      ...mapGetters(['categoryView'])
    },

3.1放大镜

  <!--放大镜效果-父组件-->
  <Zoom :skuImageList="skuInfo.skuImageList"/>
   <!--放大镜效果-子组件-->
  props:['skuImageList'],
   <img :src="skuImageList[0].imgUrl" />

获取放大镜:父传子时报错:
在这里插入图片描述
是由于传过来的skuImageList可能是空数组,会undefined,处理:
在父组件中处理:

 //给子组件的数据进行computed处理
      skuImageList(){
        return this.skuInfo.skuImageList||[]
      }
     <Zoom :skuImageList="skuImageList"/>

仍有报错:
在这里插入图片描述

:src="skuImageList[0].imgUrl"

因为skuImageList为空数组,他的【0】项undefined,他的imgUrl报错
所以子组件需要处理:

  <img :src="imgObj.imgUrl" />

  computed:{
      imgObj(){
        return this.skuImageList[0]||{}
      },

3.2 属性值[排他操作]

<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>
               
</dl>

    changeActive(SaleAttrValue,arr){
      console.log(SaleAttrValue,arr);
      arr.forEach(item => {
        item.isChecked=0
        
      });
      SaleAttrValue.isChecked=1
    }

3.3轮播图【js渲染选中状态-同级组件传递(全局事件总线)】

      <div
        class="swiper-slide"
        v-for="(slide, index) in skuImageList"
        :key="slide.id"
      >
        <img :src="slide.imgUrl" :class="{active:currentIndex==index}" @click="changeCurrentIndex(index)"/>
      </div>
import Swiper from "swiper";
export default {
  name: "ImageList",
  props: ["skuImageList"],
  data() {
    return{
      currentIndex:0
    }
  },
  watch: {
    skuImageList() {
      this.$nextTick(() => {
        new Swiper(this.$refs.cur, {
          // 如果需要前进后退按钮
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
          //显示几个图片
          slidesPerView: 2,
          slidesPerGroup: 1,
        });
      });
    },
  },
  methods:{
    changeCurrentIndex(index){
      this.currentIndex=index
      //通知同级放大镜组件的index
      this.$bus.$emit('getIndex',this.currentIndex)
    },
  },
};

 mounted() {
    this.$bus.$on("getIndex", (index) => {
      this.currentIndex = index;
    });
  },

3.4 放大镜功能实现

在这里插入图片描述

<template>
  <div class="spec-preview">
    <img :src="imgObj.imgUrl" />
    <div class="event" @mousemove="handler"></div>
    <div class="big">
      <img :src="imgObj.imgUrl" ref="big" />
    </div>
    <div class="mask" ref="mask"></div>
  </div>
</template>

export default {
  name: "Zoom",
  props: ["skuImageList"],
  data() {
    return {
      currentIndex: 0,
    };
  },
  computed: {
    imgObj() {
      return this.skuImageList[this.currentIndex] || {};
    },
  },
  mounted() {
    this.$bus.$on("getIndex", (index) => {
      this.currentIndex = index;
    });
  },
  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

      mask.style.left=left+'px';
      mask.style.top=top+'px';

      big.style.left=-2*left+'px'
      big.style.top=-2*top+'px'
    },
  },
};
</script>

3.5 加入购物车-成功页面

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

加入购物车,有三步操作

     //1.发请求-将产品加入到数据库(通知服务器)
      //2.服务器存储成功,进行路由跳转传递参数
      //3.失败:给用户进行提示

从详情页到添加到购物车成功页时,购物车成功页面也需要产品详情页的数据,可以通过请求获取数据,也可以用路由跳转,还需要把一些详情页中的商品参数传过去,也可以通过路由传参+会话存储实现。怎么办?

本地存储【local storage持久】;会话存储【session storage不持久,关闭页面就无】
因为买一件展示一件,没必要存起来,想看谁,展示谁即可。所以添加购物车成功页面,搜用会话存储。

1.路由跳转加传参:需要传的参数时{}对象形式,不是单一的数据,用query传参:但是这种格式地址栏中对象转为字符串格式,很丑,且如果是对象,新页面刷新后就获取不到了

 this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})

在这里插入图片描述
2.只用路由传个数,数据用会话存储

路由接收:在标签里:$route.query.skuNum,在js:this.$route.query.skuNum

本地存储和会话存储不允许存对像形式,可在application的session storage中查看,或打印看
存的时候:JSON.stringify(this.skuInfo)) //转为字符串形式
获取的时候:

//购物车页面
 //sessionStorage.setItem('SKUINFO',this.skuInfo)//不对,接收的是对象[object Object]
 sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))//转为字符串形式
 //添加购物车后成功页面
 mounted(){
    //  console.log(sessionStorage.getItem('SKUINFO'));//得到的是字符串形式,打印可以看出
     console.log(JSON.parse(sessionStorage.getItem('SKUINFO')));//测试下
    },
//可以在computed中直接计算得到
    computed:{
      skuInfo(){
        return JSON.parse(sessionStorage.getItem('SKUINFO'  ))//接收时转为对象形式
      },
    }

如此就在vue开发工具中查看到computed的skuInfo了

//添加购物车页
<div class="cartWrap">
              <div class="controls">
                <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 @click="addShopCar">加入购物车</a>
              </div>
 </div>

    //加入购物车
    async addShopCar(){
      //1.发请求-将产品加入到数据库(通知服务器)
      // let result=this.$store.dispatch('addOrUpadteShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})
      // console.log(result,888);//返回的是promise对象
     
      
      try {
        //当前  派发一个action,下面的代码是调用仓库的addOrUpadteShopCart方法,这个方法加上async,返回的肯定是Promise,要么成功,
        await this.$store.dispatch('addOrUpadteShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})
        //1.成功后跳转路由,跳转及传参---地址栏对象转为字符串形式,很丑
        // this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})

        //2.skuNum用路由传参,skuInfo用会话存储
        // sessionStorage.setItem('SKUINFO',this.skuInfo)//不对,接收的是对象[object Object]
        sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))//转为字符串形式

        this.$router.push({name:'addCartSuccess',query:{skuNum:this.skuNum}})
        
      } catch (error) {
        alert(error.message)
        
      } 


      //2.服务器存储成功,进行路由跳转传递参数
      //3.失败:给用户进行提示
    },
 //直接计算出skuinfo
    computed:{
      skuInfo(){
        // return sessionStorage.getItem('SKUINFO')//接收
        return JSON.parse(sessionStorage.getItem('SKUINFO'  ))//接收时转为对象形式
      },
    }

3.6 成功页面-返回查看商品详情

  <router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link>

3.7 成功页面-去购物车结算

<router-link to="/ShopCart" >去购物车结算 > </router-link>

需要携带客户信息【uuid】,先用临时游客身份,一次登录游客身份,下次再次登录游客身份,应该是同一个,不会变,且永久保存。用local storage。

点击去加入购物车时,除了带产品id,个数,还需要带着游客什么身份信息,可以通过请求头携带,在请求拦截器中使用

【浏览器npm中搜uuid,可按照说明说明和使用,因node_modules中有,所以不用再安装了】,还有nanoid

先从本地获取,没有的话,再函数生成

在src根目录下新建一个文件utils-》uuid_token.js,该文件中常放诸如【正则,游客身份等】常用功能。

这是个封装函数,要有返回值 return

import { v4 as uuidv4 } from 'uuid';
//生成一个随机数,每次执行不能发生变化,且游客身份持久保存
export const getUUID=()=>{
    //先从本地存储中获取uuid,看是否有
    let uuid_token=localStorage.getItem('UUIDTOKEN')//none
    //没有的话
    if(!uuid_token){
        //生成游客临时身份
        uuid_token=uuidv4()
        //本地存储存储一次
        localStorage.setItem('UUIDTOKEN',uuid_token)
    }
    return uuid_token;

}

存到store中,需要在仓库的detail.js中引入

//封装游客身份模块uuid,生成一个不能改变的随机数字
import {getUUID} from '@/utils/uuid_token'

const state={
    //不要乱写,看接口文档返回数据类型,是{}
    goodInfo:{},
    //游客临时身份
    uuid_token:getUUID()
   
};

然后放在请求头中,在api=>request.js中
不仅要存到store中,还需要带给服务器,在请求头中,这样每一个请求中都含有请求头—这个临时游客身份【userTempId】

//请求拦截器
requests.interceptors.request.use((config) => {
  //config:配置对象,里面有header属性
  if (store.state.detail.uuid_token) {
    //请求头添加字段【userTempId】:固定的
    config.headers.userTempId = store.state.detail.uuid_token;
  }

  nprogress.start();
  return config;
});

购物车数据有了之后,渲染静态页面,另外计算每一个产品的数量,产品小计、总价和全选

//用的value
<input  autocomplete="off"  type="text"  :value="cart.skuNum"  minnum="1"  class="itxt"  />                      

<span class="sum">{{cart.skuPrice*cart.skuNum}}</span>

 <input class="chooseAll" type="checkbox" :checked="isAllCheck" />
 
  computed: {
   //总计
   totalPrice(){
     let sum=0;
      this.cartInfoList.forEach(item => {
       sum+=item.skuPrice*item.skuNum
     });
     return sum
   },
   //全选
   isAllCheck(){
     return this.cartInfoList.every(item=>item.isChecked==1);
   },
  },

修改购物车产品数量(需发请求)

  <a href="#none" class="sindelet" @click="deleteCartById(cart)">删除</a>
          <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>
const actions={
    //将产品添加到购物车中
    async addOrUpadteShopCart({commit},{skuId,skuNum}){
        //加入购物车后(发请求),前台将参数带给服务器,服务器写入数据成功,并没有返回其他数据,知识返回code=200,表示操作成功
        //因服务器并没有返回其他数据,因此不需要在vuex三连存储数据   
        let result=await reqAddOrUpdateShopCart(skuId,skuNum)
        if(result.code==200){
            //加入购物车成功
            return 'ok'
        }else{
            //加入购物车失败
            return Promise.reject(new Error('faile'))
        }
      
    },
  

};
        handler: throttle(async function (type, disNum, cart) {
      //type区分三个元素
      //disNum变化量和最终量
      //cart产品id
      switch (type) {
        case "add":
          disNum = 1;
          break;
        case "minus":
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          //输入非法【字母,汉字】或者负数
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //小数取整
            disNum = parseInt(disNum) - cart.skuNum;
          }
          break;
      }
      //  console.log(disNum);
      //派发请求
      try {
        await this.$store.dispatch("addOrUpadteShopCart", {
          skuId: cart.skuId,
          skuNum: disNum,
        });
        this.getData();
      } catch (error) {}
    }, 800),

当快速增加或删除购物车产品数量时,出现负数【因为操作太快,导致后台数据还没回来,用节流
删除某一产品

//按需引入,因为是默认暴露的,所以不加 { throttle }大括号了
import throttle from "lodash/throttle";

   handler:throttle(  async function(type, disNum, cart){
            //type区分三个元素
      //disNum变化量和最终量
      //cart产品id
      switch (type) {
        case "add":
          disNum=1;
          break;
        case "minus":
         disNum=cart.skuNum>1?-1:0
          break;
        case "change":
          //输入非法【字母,汉字】或者负数
         if(isNaN(disNum)||disNum<1){
           disNum=0
         }else{
           //小数取证
           disNum=parseInt(disNum)-cart.skuNum
         }
          break;
      }
      //  console.log(disNum);
      //派发请求
      try {
         await this.$store.dispatch('addOrUpadteShopCart',{skuId:cart.skuId,skuNum:disNum})
         this.getData()
      } catch (error) {   
      }

    },800),

修改产品状态-删除

const actions = {
  //删除购物车商品
  async deleteCartList({commit},skuId){
    let result=await reqDeleteCartById(skuId)
    if(result.code==200){
      return 'ok'
    }else{
      return Promise.reject(new Error('faile'))
    }
  }
  
};
    async deleteCartById(cart) {
      try {
        await this.$store.dispatch("deleteCartList", cart.skuId);
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },

修改商品的勾选状态
全选按钮跟着变化

  @change="updateChecked(cart, $event)"

       //修改某产品选中状态
    async updateChecked(cart, event) {
      try {
        console.log(event.target.checked,78778);
        let isChecked = event.target.checked ? "1" : "0";
        await this.$store.dispatch("updateCheckById", {
          skuId: cart.skuId,
          isChecked: isChecked,
        });
        this.getData()
      } catch (error) {
        alert(error.message);
      }
    },

在这里插入图片描述
删除选中的全部商品
写接口,vuex,渲染
接口和删除单个接口一致。

  //删除所选商品  vuex的actions
  // deleteAllCheckedCart(context){
  //   console.log(context);//打印出来的是一个小仓库
  // },
  deleteAllCheckedCart({ dispatch, getters }) {
    let promiseAll=[]
    getters.cartList.cartInfoList.forEach((item) => {
      let promise=item.isChecked == 1 ? dispatch("deleteCartList", item.skuId) : "";
      promiseAll.push(promise)
    });
    //只有p1,p2,p3...全部成功,返回结果即为成功
    return Promise.all(promiseAll)
  },
    //删除选中的商品
    async deleteAllCheckedCart(){
      try {
       await this.$store.dispatch('deleteAllCheckedCart')
      this.getData()
      } catch (error) {
        alert(error.message)
      }
    },

全选按钮功能
同删除,注意没有数据时,全选没有被选中

 <div class="select-all">
        <input
          class="chooseAll"
          type="checkbox"
          :checked="isAllCheck&&cartInfoList.length>1"
          @change="updateAllChecked"
        />
        <span>全选</span>
      </div>
    //全选状态
    async updateAllChecked(event) {
      try {
        let isChecked = event.target.checked ? "1" : "0";
        await this.$store.dispatch("updateAllChecked", { isChecked });
        this.getData();
      } catch (error) {
        alert(error.message)
      }
    },
const actions = {
  //切换产品选中状态-没有返回数据的,要返回一个成功或失败的结果
  async updateCheckById({ commit }, { skuId, isChecked }) {
    let result = await reqUpdateCheckById(skuId, isChecked);
    if (result.code == 200) {
      return "ok";
    } else {
      return Promise.reject(new Error("faile"));
    }
  },

  updateAllChecked({dispatch,state},{isChecked}){
    let promiseAll=[]
    state.cartList[0].cartInfoList.forEach((item)=>{
      let promise=dispatch('updateCheckById',{skuId:item.skuId,isChecked})
      promiseAll.push(promise)
    })
    return Promise.all(promiseAll)
  },
};

结算(结算之前先解决登录和注册)–08
filter返回的是新数组
map返回的是新数组
find查找数组中符合条件的元素返回为最终结果
渲染:
提交订单成功
提交后,向服务发请求【支付信息】

不用vuex了。练练
在这里插入图片描述
在这里插入图片描述

api放到去全局的操作,所以所有组件内可以不引用接口,可直接使用
在这里插入图片描述
提交成功后返回订单号:进行路由跳转和传参【订单号】
在这里插入图片描述

在这里插入图片描述
支付:微信支付【√】,支付宝支付
生命周期尽量别加async:async mounted
在这里插入图片描述
点击支付,出现二维码支付
elementui
在这里插入图片描述
二维码
在这里插入图片描述
在这里插入图片描述

npm=>qrcode安装插件,应用
获取支付订单状态,要一直询问是否支付==》长连接
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
个人中心:
未完待续。。。。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值