Vue基础知识点击此处——Vue.js
这里已经滚瓜烂熟了哈哈哈哈。。。。
1、准备静态页面
2、拆分组件,配置路由信息
3、写接口,使用Vuex存储数据
4、把服务器数据渲染到页面上
这里把详情页Detail组件放到路由组件pages里src/pages/Detail/index.vue
点击商品图片 => 路由跳转并传参 => 跳转到Detail挂载完毕派发actions => 向后台发送请求拿到数据 => 把数据给组件 => 数据渲染到页面
经分析发现,在进行路由跳转时,我们要带上商品的id,这样服务器就能根据商品id去找到对应的商品信息在详情页进行展示,所以我们在配置路由规则时应该使用params
参数占位符,路由规则配置如下:
{
path: '/detail/:skuId?', //商品id的占位符,跳转时我们要带上商品参数
component: Detail,
meta: {
showFooter: true
}
},
然后使用声明式路由导航在Search页的商品图片处跳转并传参
一般来说,如果路由规则比较多,我们会把路由规则另外写一个js文件引入,这样的话路由器这边会比较清晰,方便写其他东西
// src/router/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: "/home",
component: Home,
meta: { showFooter: true },
},
{
name: "sousuo",
//使用params传参时,需要占位
path: "/search/:keyword?",
component: Search,
meta: { showFooter: true },
},
{
path: "/login",
component: Login,
meta: { showFooter: false },
},
{
path: "/register",
component: Register,
meta: { showFooter: false },
},
{
path: "/detail/:skuid",
component: Detail,
meta: { showFooter: true },
},
//重定向,在项目跑起来的时候,访问"/",立马让他定向到首页
{
path: "*",
redirect: "/home",
},
];
再在index.js里引入
在进行路由跳转时可能滚动条不在最上面。切换路由时,想要页面滚动到顶部,我们可以在路由器中添加一个scrollBehavior
函数解决这个问题,返回值是期望滚动到的位置,y:0表示跳转后滚动条距离顶部0px。
export default new VueRouter({
//配置路由规则
routes,
//配置滚动行为
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 } //期望滚动到的位置,0意思是跳转后滚动条始终在最上方
},
})
看以下接口文档:
先到api里写接口
写仓库用来actions
接收数据并state
存储数据,还有getters
简化数据。这里是新建了一个小仓库detail
,和home
、search
平级,别忘了暴露给大仓库。
这里面要注意getters中数据的写法,因为请求数据是一个异步操作,所以一开始数据是空对象,读空对象身上的属性肯定报错,服务器数据回来后,把state中的原数据替换,getters就能读到值,然后重新响应,所以页面不会有问题,但是控制台肯定会先报错的,如果不想看到这个错误,就让getters中的数据在读不到东西的时候先置空
src/store/detail/index.js
//本文件用于存储Detail详情页的数据
import { reqDetailData } from "@/api";
const state = {
detailData: {},
};
const actions = {
async getDetailData(context, skuId) {
let result = await reqDetailData(skuId);
console.log(result);
if (result.code == 200) {
context.commit("GETDETAILDATA", result.data);
}
},
};
const mutations = {
GETDETAILDATA(state, detailData) {
state.detailData = detailData;
},
};
//简化数据
const getters = {};
export default {
state,
mutations,
actions,
getters,
};
去Detail组件中派发请求并传参过去
注意:这里的skuid是大写小写,最好去开发者工具看看。
这里由于我们在进行从Search到Detail路由跳转的时候把当前商品的id传参传过去了,传给了$route
,所以我们可以通过this.$route.params.skuId
拿到商品id并把它派发给actions发送ajax请求
。(之所以是params.skuId是因为占位符写的skuId)
我们需要的数据都在state.detailData.XXX
里,如果每个都直接这样写的话很麻烦,我们可以用getters
简化一下。
src/store/detail/index.js
//简化数据
const getters = {
categoryView(state) {
//因为返回的时候可能异步操作还没把数据请求过来,所以要加个或空
//服务器数据回来后,把state中的原数据替换,getters就能读到值,然后重新响应
return state.detailData.categoryView || {};
},
skuInfo(state) {
return state.detailData.skuInfo || {};
},
spuSaleAttrList(state) {
return state.detailData.spuSaleAttrList || [];
},
};
仓库根据参数拿到后台数据后,Detail
通过mapGetters
来获取数据,然后根据数据的结构把它们展示到页面的对应位置,展示数据比较简单。
先用计算属性捞一下getters:
computed: {
...mapGetters('detail', ['categoryView', 'skuInfo', 'spuSaleAttrList'])
}
找到对应位置的对应数据:
在对应位置展示:
观察数据,发现放大镜这部分的图片数据存储在skuInfo
里的一个数组中
使用props让父组件
Detail
给子组件传值,下图左边是父组件右边是子组件
然后把数组中第一张图片拿过来展示作为默认显示,但是这样会有bug,如下
问题的关键:不能出现undefined.xxx
请求数据是异步操作,所以
getters
中的数据skuInfo
没有读到的话,会先返回空对象,然后去读取空对象身上的属性,肯定会报错,但是后边数据请求回来了,又会重新渲染重新响应,所以页面显示正常,但是控制台会先报一个错,解决办法还是加个逻辑或
把它置空一下子|| []
其实这个bug的关键就是,不能出现undefined.xxx。前三步都好理解,关键是第四步这个地方,其实空数组[0]肯定是undefined,然后undefined.imgUrl肯定是会报错的,这种情况的话可以弄一个计算属性,如果传过来的是空数组,那么skuImageList[0]就先或一个空对象(因为数组内的元素都是对象),空对象.imgUrl是undefined,这样页面没请求到数据的时候就不会报错了
这个轮播图和我们之前封装的那个全局轮播图组件样式是不一样的,所以全局组件不能拿过来用,我们这里需要自己再重新定义一下
还记得做轮播图的三大步吗?还是那几个:引包=>搭建页面=>添加js
其中第三步还是采用watch+$nextTick
,等数据请求过来且v-for
遍历生成完毕后生成swiper
实例。
有点忘记的可以参考一下我前面的笔记,原生js写的和引包的都有写——引包写法、原生JS写法。
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, // 每一次切换图片的个数
});
})
}
},
鼠标点击某小图时动态添加active样式,这里简单的话可以直接搞个hover样式,但是为了练习js,还是搞点复杂的吧。
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>
点击轮播图(ImageList组件
)的图片,就把当前图片的索引传给放大镜(Zoom组件
),那么这里就涉及到了兄弟组件通信,可以直接使用全局总线通信。
1、首先给放大镜这边绑定全局事件
2、去轮播图这边触发事件并把当前图片对象的索引值传过去
3、触发事件后执行回调,把当前索引值通过data给计算属性中的imgObj
4、vue检测到data的改变,重新解析模板,更新页面
5、注:默认放大镜data中的index是0,这样默认就会显示第一张图片。默认轮播图data中currentIndex是0,这样默认轮播图第一张图片有active样式。
我们前面在getters已经获取并简化了数据:
用mapgetters
引入一下:
在对应位置进行展示:
这里我们要实现点击某个属性时该属性高亮,其他属性变灰的效果,用到排他思想
首先应该给每个属性绑定点击事件,并且传入两个参数
第一个参数是当前售卖属性值所在的那个对象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
这些元素偏移量如果不太熟悉的话可以点此复习一下——PC端网页特效
在放鼠标移动事件的这个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';
}
这个数量的框既可以点击加减按钮操作,也可以用户自己输入
这块的话我们肯定是要收集这个商品的数量然后后边要显示计算价格啥的,所以这里的话可以使用v-model
收集一下子,然后点击加号就+1,点击减号就-1,但是这个数量不能<1,所以减号这里要加个判断。
用户可能输入任何花里胡哨的值,所以我们要对其进行校验,首先给这个输入框绑定一个onchange
事件,当加入购物车数量输入框的内容改变时离开焦点触发此函数
这里对三种情况进行校验
第一种:用户输入
非数值字符串
,让它乘以1,因为任何非数值字符串乘以1都会是NaN
第二种:用户输入负数
,这种情况和第一种情况如果有一个出现,那么就置为默认值1
第三种:用户输入小数
,直接改为取整
changeSkuNum(event) {
// 拿到用户输入的值做校验
let value = event.target.value * 1; //1.任何非数值字符串乘以1都会是NaN
if (isNaN(value) || value < 1) {
this.skuNum = 1; //2.如果用户输入非数值字符串或负数,那么就改成1
}
//3.如果用户输入正常,就向下取整(避免用户输入小数点)
else {
this.shopCarNum = parseInt(value);
}
},
详情页的笔记差不多整理到这啦,下一篇整理购物车模块相关的的笔记。