vue尚品汇商城项目-day04【28.详情页面Detail】

在这里插入图片描述

28.详情页面Detail

重难点说明:

  1. 图片放大镜效果

  2. 小图轮播

28.1点击跳转详情页面且滚轮处在顶部

编写组件4步骤:

  1. 静态页面
  2. ajax调接口
  3. vuex
  4. 动态渲染展示数据

实现步骤:

  1. 在pages文件夹下新建Detail文件夹
  2. 在src/router/index.js下新增“页面详情”路由跳转+滚动行为
  3. 在src/pages/Search/index.vue图片跳转的地方改成路由跳转

修改代码:

/router/index.js

//引入路由跳转配置文件
import routes from "./routes"

//配置路由
export default new VueRouter({
    //实际是routes:routes,但是因为同名,所以可以只保留key即可,于是就有如下的写法
    routes,
    //滚动行为
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        return { y: 0 }
    },
})

src/pages/Search/index.vue

<!-- 在路由跳转的时候切记别忘记带id(params)参数 -->
<router-link :to="`/detail/${good.id}`"><img :src="good.defaultImg" /></router-link>

src/router/routes.js

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

export default [
    {
        name: 'detail',
        path: '/detail/:skuId?',
        component: Detail,
    },
    {
        path: '/home',
        component: Home,
        meta:{"isShow": true}
    },
    {
        name: 'search',
        //:keyword?    其中的?可以理解成正则中的问号,代表出现0次或1次,这样就能进行控制params参数传递与不传递
        path: '/search/:keyword?',
        component: Search,
        meta:{"isShow": true}
    },
    {
        path: '/register',
        component: Register,
        meta:{"isShow": false}
    },
    {
        path: '/login',
        component: Login,
        meta:{"isShow": false}
    },
    //重定向,在项目跑起来的时候,访问/,立马让他定向到首页
    {
        path: '*',
        redirect: "/home",
    }
]

注意点1:“页面详情”路由跳转是需要传递参数的,传递的是商品的id,所以才有了如下写法

{
            name: 'detail',
            path: '/detail/:skuId?',
            component: Detail,
}

注意点2:

问题:点击跳转详情的时候,发现滚轮未在顶部,如何处理?

答案:vue官网提供了针对滚轮行为的API,且滚轮行为和routes是同级别的。

在这里插入图片描述

注意点3:针对编写路由跳转是可以再优化的,优化的点在于当下全部路由写在了src/router/index.js中,可能显得臃肿写,所以优化方案就是在router/下新建一个routes.js文件夹,把路由跳转相关单独提出来放在一个文件中。(其实这一步优化可有可无,看自己定夺)

src/router/index.js原始的长这样

//引入vue-router路由插件
import VueRouter from "vue-router";
//引入Vue
import Vue from "vue";
Vue.use(VueRouter);

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


//先把VueRouter原型对象的push方法,拷贝一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
//重写push|replace方法,其中第1个参数告诉原来push方法,你往哪里跳转以及传递哪些参数,第2个参数代表成功回调,第3个参数代表失败回调
VueRouter.prototype.push = function (location, resolve, reject) {
    if (resolve || reject) {
        /**
         * call || apply 区别?
         * 相同点:都可以调用函数一次,都可以篡改函数的上下文一次
         * 不同点:call与apply传递参数中,call传递参数用逗号隔开,而apply传递数组
         */
        originPush.call(this, location, resolve, reject);
    } else {
        originPush.call(this, location, ()=>{}, ()=>{});
    }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
    if (resolve || reject) {
        originReplace.call(this, location, resolve, reject);
    } else {
        originReplace.call(this, location, ()=>{}, ()=>{});
    }
}

//配置路由
export default new VueRouter({
    routes:[
        {
            name: 'detail',
            path: '/detail/:skuId?',
            component: Detail,
        },
        {
            path: '/home',
            component: Home,
            meta:{"isShow": true}
        },
        {
            name: 'search',
            //:keyword?    其中的?可以理解成正则中的问号,代表出现0次或1次,这样就能进行控制params参数传递与不传递
            path: '/search/:keyword?',
            component: Search,
            meta:{"isShow": true}
        },
        {
            path: '/register',
            component: Register,
            meta:{"isShow": false}
        },
        {
            path: '/login',
            component: Login,
            meta:{"isShow": false}
        },
        //重定向,在项目跑起来的时候,访问/,立马让他定向到首页
        {
            path: '*',
            redirect: "/home",
        }
    ],
    //滚动行为
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        return { y: 0 }
    },
})

优化后的方案,先新建src/router/routes.js文件

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

export default [
    {
        name: 'detail',
        path: '/detail/:skuId?',
        component: Detail,
    },
    {
        path: '/home',
        component: Home,
        meta:{"isShow": true}
    },
    {
        name: 'search',
        //:keyword?    其中的?可以理解成正则中的问号,代表出现0次或1次,这样就能进行控制params参数传递与不传递
        path: '/search/:keyword?',
        component: Search,
        meta:{"isShow": true}
    },
    {
        path: '/register',
        component: Register,
        meta:{"isShow": false}
    },
    {
        path: '/login',
        component: Login,
        meta:{"isShow": false}
    },
    //重定向,在项目跑起来的时候,访问/,立马让他定向到首页
    {
        path: '*',
        redirect: "/home",
    }
]

src/router/index.js

//引入vue-router路由插件
import VueRouter from "vue-router";
//引入Vue
import Vue from "vue";
Vue.use(VueRouter);
import routes from "./routes"

//先把VueRouter原型对象的push方法,拷贝一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
//重写push|replace方法,其中第1个参数告诉原来push方法,你往哪里跳转以及传递哪些参数,第2个参数代表成功回调,第3个参数代表失败回调
VueRouter.prototype.push = function (location, resolve, reject) {
    if (resolve || reject) {
        /**
         * call || apply 区别?
         * 相同点:都可以调用函数一次,都可以篡改函数的上下文一次
         * 不同点:call与apply传递参数中,call传递参数用逗号隔开,而apply传递数组
         */
        originPush.call(this, location, resolve, reject);
    } else {
        originPush.call(this, location, ()=>{}, ()=>{});
    }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
    if (resolve || reject) {
        originReplace.call(this, location, resolve, reject);
    } else {
        originReplace.call(this, location, ()=>{}, ()=>{});
    }
}

//配置路由
export default new VueRouter({
    //实际是routes:routes,但是因为同名,所以可以只保留key即可,于是就有如下的写法
    routes,
    //滚动行为
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        return { y: 0 }
    },
})

28.2产品详情数据ajax获取

使用步骤:

  1. 封装ajax调商品详情接口
  2. vuex封装
  3. 在详情挂载完毕后派发action

修改代码:

src/api/index.js

//获取产品详情信息的接口  URL: /api/item/{ skuId }  请求方式:get
export const getGoodsInfo = (skuId)=>requests({url:`/item/${skuId}`, method:"get"});

src/store/index.js

import detail from "@/store/detail"

//模块:把小仓库进行合并变为大仓库
    modules:{       
        detail
    }

src/store/detail/index.js

import {getGoodsInfo} from "@/api";
//Detail模块的小仓库
//actions代表一系列动作,可以书写自己的业务逻辑,也可以处理异步
const actions = {
    //获取产品信息的action
    async getGoodsInfo(context, skuId) {
        let response = await getGoodsInfo(skuId);
        if (response.code == 200) {
            context.commit("GET_GOODS_INFO", response.data)
        }
    }
}
//mutations代表维护,操作维护的是state中的数据,且state中数据只能在mutations中处理
const mutations = {
    GET_GOODS_INFO(state, goodsInfo) {
        state.goodsInfo = goodsInfo
    },
}
//state代表仓库中的数据
const state = {
    //仓库初始状态
    goodsInfo:{}
}
//计算属性
//项目当中getters主要的作用是:简化仓库中的数据(简化数据而生)
//可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
const getters = {}

//创建并暴露store
export default {
    actions,
    mutations,
    state,
    getters
}

src/pages/Detail/index.vue

mounted() {
      //派发action获取产品详情的信息
      this.$store.dispatch("getGoodsInfo", this.$route.params.skuId)
    }

注意点1:

问题:什么时候派发action?

答案:当用户点击图片详情,当search组件跳转到detail组件中,并且detail组件挂载完毕时进行派发。

注意点2:ajax封装的get请求和post请示是有区别的,要留意

这是GET不传参的:
export const mockGetFloorList = ()=> mockRequests.get("/floor")
-------------------------------------------------------------------------------
这是GET传参的:
export const getGoodsInfo = (skuId)=>requests({url:`/item/${skuId}`, method:"get"});
-------------------------------------------------------------------------------
这是POST传参的:
export const getSearchList = (params)=>requests({url:"/list", method:"post", data:params});

注意点3:新建商品详情src/store/detail/index.js对应的vuex文件时,要提前在src/store/index.js中注册。

注意点4:调用商品详情接口是要传skuId参数的,这个可以在路由参数params中获取,就是你在定义路由规则的时候定义的那个参数。

28.3产品详情展示动态数据

代码修改地方:

src/store/detail/index.js

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

src/pages/Detail/index.vue

<!-- 导航路径区域 -->
<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>

<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>
<!-- 小图列表 -->
<ImageList :skuImageList="skuImageList"/>
          
<h3 class="InfoName">{
   
   {skuInfo.skuName}}</h3>
<p class="news">{
   
   {skuInfo.skuDesc}}</p>      
<em>{
   
   {skuInfo.price}}</em>

<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">{
   
   {spuSaleAttrValue.saleAttrValueName}}</dd>
</dl>

computed: {
      ...mapGetters(["categoryView", "skuInfo", "spuSaleAttrList"]),
      //给子组件的数据
      skuImageList() {
        //如果服务器数据没有回来,skuInfo这个对象是空数组
        return this.skuInfo.skuImageList || []
      }
    }

src/pages/Detail/ImageList/ImageList.vue

<div class="swiper-slide" v-for="(skuImage, index) in skuImageList" :key="index">
        <img :src="skuImage.imgUrl">
</div>

 props: ["skuImageList"]

src/pages/Detail/Zoom/Zoom.vue

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

props: ["skuImageList"],
    computed: {
      coverSkuImageListObj() {
        //如果服务器数据没有回来,skuImageList子项应该是空对象
        return this.skuImageList[0] || {}
      }
    }

注意点1:

问题:功能都好使,为啥会有警告?

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

答案:因为goodInfo未调用接口是是个空对象,而空对象调用categoryView就是underfine,而underfine再调用category1Name就会报错,所以正确应该返回空对象,正确代码如下:

skuInfo(state) {
        return state.goodsInfo.skuInfo || {};
    }

注意点2:

问题:在修改放大镜功能的时候,警告报错但不影响功能,为什么?

在这里插入图片描述

在这里插入图片描述

答案:Zoom组件在初始化加载的时候仓库中是没有数据的,还为没有调服务器接口呢所以压根没数据,所以模板代码中空对象调用属性的时候就会报错。正确写法如下:

<!--放大镜效果-->
<Zoom :skuImageList="skuImageList"/>

computed: {
	//给子组件的数据
      skuImageList() {
        //如果服务器数据没有回来,skuInfo这个对象是空数组
        return this.skuInfo.skuImageList || []
      }
}

注意点3:

问题:报错如图

在这里插入图片描述

在这里插入图片描述

答案:skuImageList空数组所以skuImageList[0]就是undefine,而undefine不可能调用属性imgUrl,所以报错。正确写法如下:

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

computed: {
      coverSkuImageListObj() {
        //如果服务器数据没有回来,skuImageList子项应该是空对象
        return this.skuImageList[0] || {}
      }
    }

28.4实现商品详情属性的勾选切换,效果如图

在这里插入图片描述

修改代码:

src/pages/Detail/index.vue

<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) {
        //遍历全部售卖属性值isChecked为零没有高亮了
        spuSaleAttrValueList.forEach(item =>{item.isChecked = '0'})
        //点击的那个售卖属性值变为1
        spuSaleAttrValue.isChecked = '1';
      }
}

注意点1:

问题:为啥点击方法要传递2个参数spuSaleAttrValue, spuSaleAttrValueList?

答案:spuSaleAttrValue其实就是数组spuSaleAttrValueList的子项,点击选中时是要把数组spuSaleAttrValueList中的选中项对象中的isChecked属性值设置为1才行。

注意点2:

问题:如何切换选中高亮呢?

答案:点击方法刚进来把所有选项都设置为不勾选,当选中谁就在对应子项的isChecked属性值为1就行。

28.5实现放大镜下方轮播图切换及点击展示不同图片功能

想实现的效果如下图:

在这里插入图片描述

修改代码:

src/pages/Detail/Zoom/Zoom.vue

data() {
      return {
        currentIndex: 0
      }
    },
    computed: {
      coverSkuImageListObj() {
        //如果服务器数据没有回来,skuImageList子项应该是空对象
        return this.skuImageList[this.currentIndex] || {}
      }
    },
    mounted() {
      //全局事件总线:获取兄弟组件传递过来的索引值
      this.$bus.$on('transferCurrentIndex', (currentIndex)=>{
        //修改当前响应式数据
        this.currentIndex = currentIndex;
      })
    }

src/pages/Detail/ImageList/ImageList.vue

<img :src="skuImage.imgUrl" :class="{active: currentIndex == index}" @click="changeCurrentIndex(index)">

data() {
      return {
        currentIndex: 0
      }
    },
    methods: {
      changeCurrentIndex(index) {
        //修改响应式数据
        this.currentIndex = index;
        //通知兄弟组件:当前的索引值为几
        this.$bus.$emit('transferCurrentIndex', index)
      }
    },
    watch: {
      //监听数据:可以保证数据一定ok,但是不能保证v-for遍历结构是否完事。
      skuImageList: {
        handler(newValue, oldValue) {
          this.$nextTick(() => {
            new Swiper('.swiper-container', {
              //显示几个图片设置
              slidesPerView: 3,
              //每一次切换图片个数
              slidesPerGroup: 1,
              // 如果需要前进后退按钮
              navigation: {
                nextEl: '.swiper-button-next',
                prevEl: '.swiper-button-prev',
              },
            })
          });
        }
      }
    }
    
<style lang="less" scoped>
	//该处这是掉了,不通过CSS方式鼠标划入添加选中高亮色,想通过JS方式实现
        //&:hover {
        //  border: 2px solid #f60;
        //  padding: 1px;
        //}
</style>

注意点1:放大镜下方的还是轮播图,轮播图的注意事项请看知识点“21.使用swiper轮播图插件”

注意点2:

问题:swipper如何控制,一排显示几个以及滚动跳过几个?

答案:请找官网
在这里插入图片描述

注意点3:

问题:给轮播图选中高亮显示如何做?

答案:方法有2种

第一种CSS方式鼠标划入添加选中高亮色
&:hover {
          border: 2px solid #f60;
          padding: 1px;
        }
----------------------------------------------------------------   
第二种通过JS方式实现,添加点击事件
<img :src="skuImage.imgUrl" :class="{active: currentIndex == index}" @click="changeCurrentIndex(index)">

data() {
      return {
        currentIndex: 0
      }
},

changeCurrentIndex(index) {
        //修改响应式数据
        this.currentIndex = index;
        //通知兄弟组件:当前的索引值为几
        this.$bus.$emit('transferCurrentIndex', index)
      }

28.6放大镜功能

最终效果如下:

在这里插入图片描述

修改代码:

src/pages/Detail/Zoom/Zoom.vue

<div class="event" @mousemove="handler"></div>
<div class="big">
<img :src="coverSkuImageListObj.imgUrl" ref="big"/>
</div>
<!-- 遮罩层:就是浅绿色的正方形 -->
<div class="mask" ref="mask"></div>

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';
      },
}

注意点1:放大镜功能实际和4个参数有关,分别是:【 event.offsetX、event.offsetY、mask.offsetWidth、mask.offsetHeight】。

注意点2:

问题:为啥left和top是要除以2?

let left = event.offsetX - mask.offsetWidth/2;
let top = event.offsetY - mask.offsetHeight/2;

答案:长度如图。

在这里插入图片描述

注意点3:注意约束范围,否则的话绿色正常性会出界。

//约束范围
if(left <= 0) left = 0;
if(left >= mask.offsetWidth) left = mask.offsetWidth;
if(top <= 0)top = 0;
if(top >= mask.offsetHeight) top = mask.offsetHeight;

注意点4:右面的放大的图片也要修改left、top坐标,且通过读取CSS样式发现big的宽高是mask的两倍,所以才有了如下的代码:

//修改元素的left|top属性值
mask.style.left = left + 'px';
mask.style.top = top + 'px';
big.style.left = - 2 * left + 'px';
big.style.top = -2 * top + 'px';

28.7购买产品个数的操作

在这里插入图片描述

考虑点:

  • 考虑点1:点击“-”最小只能为1,不能为0或者负数
  • 考虑点2:既然数量用户可以输入,那么就得加校验规则,正确就直接显示,不正确就显示1。比如用户输入数字和英文组合、负数、0、正小数、中英文、特殊符号等等。

修改代码:

src/pages/Detail/index.vue

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

data() {
      return {
        skuNum: 1
      }
},
//表单元素修改产品个数
changeSkuNum(event) {
        //用户输入进来的文本 * 1
        let value = event.target.value * 1;
        if (isNaN(value) || value < 1) {
          this.skuNum = 1;
        } else {
          //正常大于1【大于1整数不能出现小数】
          this.skuNum = parseInt(value);
        }
      }

注意点0:

问题:如何获取用户输入的值呢?

答案:通过event.target.value获取值。

注意点1:确保点击减号“-”最小值为1

<a href="javascript:" class="mins" @click="skuNum > 1 ? skuNum-- : (skuNum = 1)">-</a>

注意点2:

问题:如何校验用户输入数量呢?

答案:添加@change方法。

注意点3:

问题:如何判断输入值是否合规呢?

答案:

//用户输入进来的文本 * 1
let value = event.target.value * 1;
if (isNaN(value) || value < 1) {
	this.skuNum = 1;
} else {
	//正常大于1【大于1整数不能出现小数】
	this.skuNum = parseInt(value);
}

注意点4:一个小技巧:任何带非数字的字符串*1,值都为NaN。

注意点5:一个小技巧:parseInt()可以让小数向下取整。

本人其他相关文章链接

1.vue尚品汇商城项目-day04【24.点击搜索按钮跳转后的页面商品列表、平台售卖属性动态展示(开发Search组件)】
2.vue尚品汇商城项目-day04【25.面包屑处理关键字】
3.vue尚品汇商城项目-day04【26.排序操作(难点)】
4.vue尚品汇商城项目-day04【27.分页器静态组件(难点)】
5.vue尚品汇商城项目-day04【28.详情页面Detail】
6.vue尚品汇商城项目-day04【29.加入购物车操作(难点)】

猜你喜欢

转载自blog.csdn.net/a924382407/article/details/129907372