@[toc]
28.详情页面Detail
重难点说明:
1) 图片放大镜效果
2) 小图轮播
28.1点击跳转详情页面且滚轮处在顶部
编写组件4步骤:
- 静态页面
- ajax调接口
- vuex
- 动态渲染展示数据
实现步骤:
- 在pages文件夹下新建Detail文件夹
- 在src/router/index.js下新增“页面详情”路由跳转+滚动行为
- 在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获取
使用步骤:
- 封装ajax调商品详情接口
- vuex封装
- 在详情挂载完毕后派发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.加入购物车操作(难点)】