Vue2项目前台开发:第四章
一、项目开发的步骤
1、准备静态页面
2、拆分组件,配置路由信息
3、写接口,使用Vuex存储数据
4、把服务器数据渲染到页面上
这里把详情页Detail组件放到路由组件pages里src/pages/Detail/index.vue
二、配置路由规则+滚动行为
点击商品图片 => 路由跳转并传参 => 跳转到Detail挂载完毕派发actions => 向后台发送请求拿到数据 => 把数据给组件 => 数据渲染到页面
1.配置路由规则
经分析发现,在进行路由跳转时,我们要带上商品的id,这样服务器就能根据商品id去找到对应的商品信息在详情页进行展示,所以我们在配置路由规则时应该使用params
参数占位符,路由规则配置如下:
{
name: 'xiangqing',
path: '/detail/:skuId?', //商品id的占位符,跳转时我们要带上商品参数
component: Detail,
meta: {
showFooter: true
}
},
然后使用声明式路由导航在Search页的商品图片处跳转并传参
PS:一般来说,如果路由规则比较多,我们会把路由规则另外写一个js文件引入,这样的话路由器这边会比较清晰,方便写其他东西
2.滚动行为
在进行路由跳转时可能滚动条不在最上面,我们可以在路由器中添加一个scrollBehavior
函数解决这个问题,返回值是期望滚动到的位置,y:0表示跳转后滚动条距离顶部0px
const router = new VueRouter({
//配置路由规则
routes: routes,
//配置滚动行为
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 } //期望滚动到的位置,0意思是跳转后滚动条始终在最上方
},
})
三、请求详情页数据并展示数据
看下接口文档:
1.写接口
2.写Vuex仓库
写仓库用来actions
接收数据并state
存储数据,还有getters
简化数据。这里是新建了一个小仓库detail
,和home
、search
平级,别忘了暴露给大仓库。
这里面要注意
getters
中数据的写法,因为请求数据是一个异步操作
,所以一开始数据是空对象,读空对象身上的属性肯定报错,服务器数据回来后,把state中的原数据替换,getters就能读到值,然后重新响应,所以页面不会有问题,但是控制台肯定会先报错的,如果不想看到这个错误,就让getters中的数据在读不到东西的时候先置空
src/store/detail/index.js
//本文件用于存储Detail详情页的数据
import { reqDetailData } from '@/api';
export const detail = {
namespaced: true,
state: {
detailData: {}
},
actions: {
async getDetailData(context, skuId) {
let result = await reqDetailData(skuId);
if (result.code === 200) {
context.commit('GETDETAILDATA', result.data);
}
}
},
mutations: {
GETDETAILDATA(state, value) {
state.detailData = value;
}
},
//简化数据
getters: {
categoryView(state) {
//因为返回的时候可能异步操作还没把数据请求过来,所以要加个或空
//服务器数据回来后,把state中的原数据替换,getters就能读到值,然后重新响应
return state.detailData.categoryView || {};
},
skuInfo(state) {
return state.detailData.skuInfo || {};
},
spuSaleAttrList(state) {
return state.detailData.spuSaleAttrList || [];
}
}
}
3.派发actions
去Detail组件中派发请求并传参过去
这里由于我们在进行从Search
到Detail
路由跳转的时候把当前商品的id传参传过去了,传给了$route
,所以我们可以通过this.$route.params.skuId
拿到商品id
并把它派发给actions
发送ajax请求。(之所以是params.skuId
是因为占位符写的skuId
)
4.组件拿到后台返回的数据并渲染到页面
仓库根据参数拿到后台数据后,Detail
通过mapGetters
来获取数据,然后根据数据的结构把它们展示到页面的对应位置,展示数据比较简单,就不多说了。
computed: {
// ...mapState('detail', ['detailData']),
...mapGetters('detail', ['categoryView', 'skuInfo', 'spuSaleAttrList'])
}
四、放大镜展示数据
1.Detail父组件把图片数据传给放大镜子组件
观察数据,发现放大镜这部分的图片数据存储在skuInfo
里的一个数组中
使用props让父组件
Detail
给子组件传值,下图左边是父组件右边是子组件
然后把数组中第一张图片拿过来展示作为默认显示,但是这样会有bug,如下
2.解决数据未请求到时获取其属性报错的问题
问题的关键:不能出现undefined.xxx
请求数据是异步操作,所以
getters
中的数据skuInfo
没有读到的话,会先返回空对象,然后去读取空对象身上的属性,肯定会报错,但是后边数据请求回来了,又会重新渲染重新响应,所以页面显示正常,但是控制台会先报一个错,解决办法还是加个逻辑或
把它置空一下子|| []
其实这个bug的关键
就是,不能出现undefined.xxx
。前三步都好理解,关键是第四步这个地方,其实空数组[0]肯定是undefined
,然后undefined.imgUrl
肯定是会报错的,这种情况的话可以弄一个计算属性,如果传过来的是空数组,那么skuImageList[0]
就先或一个空对象(因为数组内的元素都是对象),空对象.imgUrl
是undefined
,这样页面没请求到数据的时候就不会报错了。
五、放大镜下边的轮播图展示数据
这个轮播图和我们之前封装的那个全局轮播图组件样式是不一样的,所以全局组件不能拿过来用,我们这里需要自己再重新定义一下子
1.轮播图三大步
还是那三个步骤,引包 => 搭建页面 => 添加js
其中第三步还是采用watch+$nextTick
,等数据请求过来且v-for
遍历生成完毕后生成swiper
实例
watch: {
//监听skuImageList数据的变化
// 因为它会有一个从空数组变成有数据的过程(一切都因为请求数据是一个异步操作)
skuImageList(newVal, oldVal) {
//nextTick能保证页面结构先渲染出来,然后再执行回调函数
this.$nextTick(function () {
new Swiper(".swiper-container", {
//前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
slidesPerView: 4, // 显示几个图片设置
slidesPerGroup: 1, // 每一次切换图片的个数
});
})
}
},
2.动态添加active样式(不用hover)
鼠标点击某个小图时动态添加active样式,和三级联动那块儿那个操作一样,不用css的hover,搞点高级的东西
src/pages/detail/ImageList
<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>
3.放大镜和轮播图实现梦幻联动
点击轮播图(ImageList组件
)的图片,就把当前图片的索引传给放大镜(Zoom组件
),那么这里就涉及到了兄弟组件通信
1、首先给放大镜这边绑定全局事件
2、去轮播图这边触发事件并把当前图片对象的索引值传过去
3、触发事件后执行回调,把当前索引值通过data
给计算属性中的imgObj
4、vue
检测到data
的改变,重新解析模板,更新页面
5、注:默认放大镜data中的index是0,这样默认就会显示第一张图片。默认轮播图data中currentIndex是0,这样默认轮播图会选中第一张图片。
六、产品售卖属性的排他操作
这里我们要实现点击某个属性时该属性高亮,其他属性变灰的效果,用到排他思想
首先应该给每个属性绑定点击事件,并且传入两个参数
第一个参数是当前售卖属性值所在的那个对象spuVal
,第二个参数是所有售卖属性对象所在的数组spu.spuSaleAttrValueList
然后这里由于active
样式我们是动态添加的,它是否展示取决于spuVal
对象的isChecked
属性是1还是0,这里都是我们从服务器拿过来的属性且配置到了计算属性里,所以一旦它们里面的数据改变,vue就会重新解析模板,这样的话我们就可以通过修改isChecked
属性来控制active
样式是否显示,所以回调可以这么写
methods: {
//产品售卖属性切换,排他思想
changeActive(spuVal, spuValArr) {
//第一个参数是当前售卖属性值所在的对象,第二个参数是所有售卖属性对象所在的数组
spuValArr.forEach(el => {
el.isChecked = '0'; //先把数组中每个对象的active样式去掉
spuVal.isChecked = '1'; //再给当前点击的对象添加active样式
});
}
}
七、放大镜效果实现
pageX
: 页面X坐标位置
pageY
: 页面Y坐标位置
offsetX
:鼠标坐标到元素的左侧的距离
offsetY
:鼠标坐标到元素的顶部的距离
offsetLeft
: 该元素外边框距离包含元素内边框左侧的距离
offsetTop
:该元素外边框距离包含元素内边框顶部的距离
offsetWidth
: width + padding-left + padding-right + border-left + border-right
offsetHeight
: height + padding-top + padding-bottom + border-top + border-bottom
在放鼠标移动事件的这个div里绑定鼠标移动事件
看看注释吧,反正就是计算一下,记得修改style要加px
这里还要注意右边的放大图这里,图片的top和left
要多一倍(因为放大图是放大一倍),而且要和这块儿绿东西的top和left
反的着来(移动图片,多余的部分是overflow:hidden
的)。绿框往右下角动,图片往左上角动才对
handlerMask(e) {
//1.获取这个遮罩层的dom元素
let mask = this.$refs.mask;
//2.计算定位的left和top
let x = e.offsetX - mask.offsetWidth / 2;
let y = e.offsetY - mask.offsetWidth / 2;
//3.约束条件防止盒子跑出去
if (x < 0) x = 0; //防止从左边跑出去
if (x > mask.offsetWidth) x = mask.offsetWidth; //防止从右边跑出去
if (y < 0) y = 0; //防止从上边跑出去
if (y > mask.offsetWidth) y = mask.offsetWidth; //防止从下边跑出去
//4.修改dom样式
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';
}
八、购买产品个数的加减操作
这个数量的框既可以点击加减按钮操作,也可以用户自己输入
1.收集数量并控制个数>0
这块儿的话我们肯定是要收集这个商品的数量然后后边还要显示然后计算价格啥的,所以这里的话要使用v-model
收集一下子,然后点击加号就+1,点击减号就-1,但是这个数量不能<1,所以减号这里要加个判断
<!-- 用户加减数量的操作 -->
<div class="cartWrap">
<div class="controls">
<input autocomplete="off" class="itxt" v-model="shopCarNum">
<a href="javascript:" class="plus" @click="shopCarNum++">+</a>
<a href="javascript:" class="mins" @click="shopCarNum>1?shopCarNum--:shopCarNum=1">-</a>
</div>
<div class="add">
<a href="javascript:">加入购物车</a>
</div>
</div>
......
data() {
return {
// 用户加入购物车的商品个数
shopCarNum: 1
}
},
2.校验用户自己输入的值
用户可能输入任何花里胡哨的值,所以我们要对其进行校验,首先给这个输入框绑定一个onchange
事件,当加入购物车数量输入框的内容改变时离开焦点触发此函数
这里对三种情况进行校验
第一种:用户输入
非数值字符串
,让它乘以1,因为任何非数值字符串乘以1都会是NaN
第二种:用户输入负数
,这种情况和第一种情况如果有一个出现,那么就置为默认值1
第三种:用户输入小数
,直接改为取整
//onchange事件:加入购物车数量输入框的内容改变时离开焦点触发此函数
changeShopCarNum() {
//拿到用户输入的值做校验
let value = this.shopCarNum * 1; //1.任何非数值字符串乘以1都会是NaN
if (isNaN(value) || value < 1) {
this.shopCarNum = 1; //2.如果用户输入非数值字符串或负数,那么就改成1
}
//3.如果用户输入正常,就向下取整(避免用户输入小数点)
else {
this.shopCarNum = parseInt(value);
}
}
详情页结束,下一步实现加入购物车功能,奥里给!!