文章目录
- 一、配置Detail路由
- 1. 将Detail组件配置为路由组件
- 2. 将路由配置文件拆分
- 3. 声明式导航跳转到Detail
- 跳转时存在的问题:页面滚动条还在下边
- 二、配置API及vuex
- 三、放大镜及下方轮播图
- 1. Detail组件传递放大镜数据
- 2. 读取vuex数据的经典错误undefined
- 3. 放大镜放大效果
- 3. 放大镜下方的轮播图
- 数据
- 轮播图
- 点击高亮
- 4. 放大镜与轮播图联动
- 四、 选择售卖属性
- 1. 渲染页面
- 2. 排他思想
- 五、购买产品个数的加减操作
一、配置Detail路由
1. 将Detail组件配置为路由组件
当点击某个商品调转到详情页时,需要将这个商品的id传递过去。所以这里配置了params占位。
2. 将路由配置文件拆分
就是将路由配置项里的内容提取到另一个文件中,这样更清晰。
3. 声明式导航跳转到Detail
Search组件中,将原来的a标签替换为router-link
,记得携带params参数,将商品Id传过去。
在跳转路径上可以检查传递的参数是否正确:
跳转时存在的问题:页面滚动条还在下边
正常情况,跳转到新页面时,应该处于新页面的最前面。
从图中可以看出,页面跳转前滚动条处于底部。页面跳转后滚动条还是处于底部。所以这是个问题:
解决方法:查看官网:滚动行为
在路由配置里添加一个新的配置项:
// router/index.js
export default new VueRouter({
routes,
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
return { y: 0 }
}
})
二、配置API及vuex
根据接口文档,写获取商品详情的api
//api/index.js
// 获取商品详情 url:/api/item/{商品id} 请求方式 get
export const reqGoodsInfo = (goodsId) => {
return requests({ url: `/item/${goodsId}`, method: 'get' })
}
获取来的数据依然是存在仓库里,detail又是一个模块。建立detail小仓库存储这个模块的信息
// store/detail/index.js
import { reqGoodsInfo } from "@/api";
export default {
namespaced: true,
state: {
goodsInfo: {}
},
actions: {
async getGoodsInfo (context, goodsId) {
const result = await reqGoodsInfo(goodsId)
if (result.code === 200) {
context.commit('GETGOODSINFO', result.data)
}
}
},
mutations: {
GETGOODSINFO (state, goodsInfo) {
state.goodsInfo = goodsInfo
}
},
getters: { }
}
不要忘记把这个小仓库放在大仓库里
三、放大镜及下方轮播图
1. Detail组件传递放大镜数据
在vuex中,通过getters简化获取到goodsList
里的skuInfo
。Detail组件获取到后,将该信息传递给子组件Zoom
// store/detail/index.js
getters: {
skuInfo (state) {
return state.goodsInfo.skuInfo
}}
左边Detail父组件,右边Zoom子组件.(这里先写死数据,后续再改)
2. 读取vuex数据的经典错误undefined
问题:根据提示可知是Zoom
组件里的skuImageList[0]
有问题。skuImageList
是父组件传递的。经过mounted函数测试发现,父组件传递过来的是undefined
。
因为请求数据是异步操作,如果还没有数据,
getters
中的数据skuInfo
返回的是空对象。去读空对象身上的属性,则会报错为undefined。所以skuInfo.skuImageList
是undefined。
解决:
<!--如果不存在这个数据,就给子组件Zoom传递一个空数组-->
<Zoom :skuImageList="skuInfo.skuImageList || []" />
然后又报错:
在Zoom组件中读取了
skuImageList[0].imageUrl
。skuImageList
是空数组,那么skuImageList[0]
就已经是undefined
了 ,所以读取imageUrl
也会报错了。
解决
思路同上,这里给一个空对象
// Zoom组件
computed: {
imgObj () {
return this.skuImageList[0] || {}
}
}
3. 放大镜放大效果
offsetX
:鼠标坐标到元素的左侧的距离
offsetY
:鼠标坐标到元素的顶部的距离
offsetLeft
: 元素与定位父级元素的左侧的距离
offsetTop
:元素与定位父级元素的顶侧的距离
offsetWidth
: 元素自身宽度(width + padding+border)
offsetHeight
: 元素自身高度 (height + padding+border)
回顾博客:WebAPI(二) offset
这里主要包含两个步骤:
(1)、鼠标移动,mask跟着移动
(2)、右侧的放大效果实时变化
首先给mask加一个鼠标移动事件,为了后续方便对节点操作,同时加上ref属性。(为什么给event加鼠标移动事件?)
具体业务逻辑看代码吧,结合注释计算一下,注意不要忘了修改属性的时候加单位px
。
处理第二个步骤时,需要注意,鼠标的移动与右侧大图呈现的方向是相反的。所以要加负号(加负号注意)。
<script>
handlerMask (event) {
// 1. 获取mask节点
let mask = this.$refs.mask
// 2. 修改位置,不要忘了加单位!(可以用鼠标的位置是因为鼠标总位于mask 的中心)
let x = event.offsetX - mask.offsetWidth / 2 // x水平方向,确定位置,看左侧
let y = event.offsetY - mask.offsetHeight / 2 // y垂直方向,确定位置,看上边
// 3. 加限制
if (x < 0) x = 0 // 小于0,mask就从左边出去了
if (x > mask.offsetWidth) x = mask.offsetWidth // mask的宽是父级元素的一半,如果x坐标超过了这一半,说明右边出去了
if (y < 0) y = 0 // 小于0,mask就从下边出去了
if (y > mask.offsetHeight) y = mask.offsetHeight // mask的高是父级元素的一半,如果y坐标超过了这一半,说明上边出去了
// 4. 修改绿色mask的位置
mask.style.left = x + 'px'
mask.style.top = y + 'px'
// 放大图的地方
let big = this.$refs.big
big.style.top = -2 * y + 'px'
big.style.left = -2 * x + 'px'
}
</script>
3. 放大镜下方的轮播图
回顾之前做过的轮播图(和之前的有点不一样(同时展示三张图片),所以这里不用当初定义的全局组件了):Vue2电商项目(二)、Home模块轮播图
数据
由于这里用到的数据和Zoom
(放大镜)一样,所以同样由Detail
组件传递给ImageList
组件。
<!-- Detail组件,小图列表 -->
<ImageList :skuImageList="skuInfo.skuImageList || []" />
轮播图
引包、引样式、创建实例。创建实例仍然在watch
中。
注意v-for
应该加在哪,ref
应该加在哪!!!
watch: {
// 监听数据:可以保证数据一定ok,但是不能保证v-for遍历结构是否完事儿
//监听skuImageList数据的变化,会有一个从空数组变成有数据的过程(一切都因为请求数据是一个异步操作)
skuImageList () {
this.$nextTick(() => {
new Swiper(this.$refs.mySwiper, {
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
slidesPerView: 3, // 设置能够同时显示的slides数量
slidesPerGroup: 1, // 每一次切换图片的个数
})
})
}
}
点击高亮
不用css,改用js实现点击高亮效果。这个和TypeNav
组件里的情况一致。
高亮时,添加active
这个class类。
<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>
<script>
data () {
return {
currentIndex: 0 // 默认是第0张图
}
},
methods: {
changeCurrentIndex (index) {
this.currentIndex = index
}
},
</script>
4. 放大镜与轮播图联动
点击轮播图里的某张图,放大镜的主图里就展示某张图。很明显,这两个组件是兄弟组件,所以涉及到组件间通信。采用全局事件总线的方式。
传递数据:ImageList
组件,传递的是被点击的图片对象的id。
接收数据:Zoom
组件。
ImageList
组件中用到了之前高亮写的函数。当点击图片时,显示高亮,并通过事件总线 将该图片的索引值传递过去。
四、 选择售卖属性
1. 渲染页面
// vuex小仓库里
getters: {
spuSaleAttrList (state) {
return state.goodsInfo.spuSaleAttrList || []
}
}
2. 排他思想
需求:点击某一个属性时,该属性高亮,其他属性都是灰的。
排他思想:(有同一组元素,想要某一个元素实现某种样式,需要用到循环的排他思想)
(1) 所有的元素全部清除样式(干掉其他人)
(2) 给当前元素设置样式(留下自己)
(3) 注意顺序不能颠倒,首先干掉其他人,再设置自己。
参考博客:JS排他思想
添加点击事件:
数据结构:
[
// 第一个属性
{
id:1
saleAttrName:'颜色',
// 属性值对象数组
spuSaleAttrValueList:[
{id:1,valueName:'亮黑色',isChecked:'0'},
{id:2,valueName:'粉色',isChecked:'1'},
{id:3,valueName:'白色',isChecked:'1'},
]
},
//第二个属性
...
]
思路分析: 首先要获取到当前选中的元素对象(比如值为白色的对象),给该对象的isChecked
赋值,使其高亮。其次还要获取到这个元素对象所在的数组(即spuSaleAttrValueList),对该数据里的其他对象进行遍历,将isChecked
赋值,使其不高亮。
methods: {
// 售卖属性的排他操作
// 第一个参数是选中的对象,第二个是这个对象所在的数组
changeActive (currentValue, spuValueList) {
spuValueList.forEach(el => {
// 先都赋值为0,不高亮
el.isChecked = '0'
});
// 选中的赋值为1,高亮
currentValue.isChecked = '1'
}
}
五、购买产品个数的加减操作
需求:点击+:数字加1;点击-:数字减1;输入框里可输入产品数量。
在data
里定义商品数量属性skuNum
,默认值是1;+和- 需要注意的就是减的时候商品数量不能小于1。
输入框的回调函数里需要处理用户一些非法的输入。非法输入有这样几种情况:输入文本,负数,小数;
// 改变商品个数
changeSkuNum (e) {
// 文本*1 是NaN
let value = e.target.value * 1
// 非法情况: 出现NaN或者小于1
if (isNaN(value) || value < 1) {
this.skuNum = 1
} else {
// 正常大于1 【不能出现小数】
this.skuNum = parseInt(value)
}
}