尚硅谷VUE项目-前端项目问题总结07---产品详情页
四个步骤:
1.静态组件(详情页还未注册为路由组件)
2.发请求
3.vuex
4.动态展示组件
1.静态组件(详情页还未注册为路由组件)
父组件index,两个子组件【放大镜和小图】
点击商品时,跳转到详情页,路由跳转需要传参【产品id】给详情页
在router-index中引入和添加
import Detail from '@/pages/Detail'
{
path: '/detail/:skuid',//params传参,:占位
component: Detail,
meta: {
show: true,
},
},
<router-link :to="`/detail/${item.id}`">
<img :src="item.defaultImg" />
</router-link>
知识点:
1.把router-》index中routes数组摘出来放在routes.js中统一管理,然后再引入即可
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: '*',
redirect: '/home',
},
{
path: '/home',
component: Home,
meta: {
show: true,
},
},
{
path: '/detail/:skuid',//params传参,:占位
component: Detail,
meta: {
show: true,
},
},
{
path: '/search/:keyword?',//:params传参的占位;?参数可传可不传
component: Search,
meta: {
show: true,
},
name: 'search',//传参方式为对象,用params传参
// props:true, //路由组件传props数据 第一种方法:布尔值写法-只能是params
// props:{a:1,b:2}, //路由组件传props数据 第二种方法:对象 额外的给路由组件传递一些props
// props:($route)=>{//路由组件传props数据 第三种方法:函数 可以把params和query参数,通过props传递给路由组件
// return {keyword:$route.params.keyword,joan:$route.query.joan}
// },
// 简写
props: ($route) => ({ keyword: $route.params.keyword, joan: $route.query.joan }),
},
{
path: '/login',
component: Login,
meta: {
show: false,
},
},
{
path: '/register',
component: Register,
meta: {
show: false,
},
},
]
router-》index.js
import routes from './routes'
//配置路由
export default new VueRouter({
//配置路由
// routes: routes
routes
})
2.点开详情,定位到最顶部
路由解决滚动行为
vue官网=》生态系统=》Vue Router =.>滚动行为
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 } //vue3
return {y:0} //vue2
},
})
export default new VueRouter({
//配置路由
// routes: routes
routes,
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
// return { top: 0 } //vue3
return { y: 0 } //vue2
},
//限制单个路由可以通过to属性来判断:if to.name == detail 在返回
})
2.发请求
//获取商品详情 /api/item/{ skuId } get
export const reqGoodsInfo=(skuId)=>requests({url:`/item/${ skuId }`,method:'get'})
3.vuex-获取产品详情信息
重新建一个store-》details.js文件存储信息【仍然是四件套】
const state={};
const mutations={};
const actions={};
const getters={};
//对外暴露一个对象
export default{
state,
mutations,
actions,
getters,
}
合并到index中:引入,注册
import detail from './detail'
export default new Vuex.Store({
//没有模块式开发,都写在这
// state,
// mutations,
// actions,
// getters,
// modules,
//实现Vuex仓库【模式开发】存储数据
//模块:把小仓库进行合并变为大仓库
modules:{
home,
search,
detail,
}
})
在actions中发请求:接口reqGoodsInfo
import {reqGoodsInfo} from '@/api'
const state={
//不要乱写,看接口文档返回数据类型,是{}
goodInfo:{},
};
const mutations={
GETGOODINFO(state,goodInfo){
state.goodInfo=goodInfo;
}
};
const actions={
//获取产品的action
async getGoodInfo({commit},skuid){
let result=await reqGoodsInfo(skuid)
if(result.code==200){
commit('GETGOODINFO',result.data)
}
},
};
const getters={};
//对外暴露一个对象
export default{
state,
mutations,
actions,
getters,
}
派发actions
mounted(){
//点击产品进入详情页,路由跳转时携带skuId
this.$store.dispatch('getGoodInfo',this.$route.params.skuid)
},
因为goodInfo数据复杂,所以在getters计算后返回
const getters={
categoryView(state){
return state.goodInfo.categoryView||{}
},
skuInfo(state){
return state.goodInfo.skuInfo||{}
},
spuSaleAttrList(state){
return state.goodInfo.spuSaleAttrList||[]
},
};
子组件获取数据
computed:{
//没用命名空间用下方的方式,[如果开启了命名空间的这样拿 ...mapGetters('search',['goodsList'])]
...mapGetters(['categoryView'])
},
3.1放大镜
<!--放大镜效果-父组件-->
<Zoom :skuImageList="skuInfo.skuImageList"/>
<!--放大镜效果-子组件-->
props:['skuImageList'],
<img :src="skuImageList[0].imgUrl" />
获取放大镜:父传子时报错:
是由于传过来的skuImageList可能是空数组,会undefined,处理:
在父组件中处理:
//给子组件的数据进行computed处理
skuImageList(){
return this.skuInfo.skuImageList||[]
}
<Zoom :skuImageList="skuImageList"/>
仍有报错:
:src="skuImageList[0].imgUrl"
因为skuImageList为空数组,他的【0】项undefined,他的imgUrl报错
所以子组件需要处理:
<img :src="imgObj.imgUrl" />
computed:{
imgObj(){
return this.skuImageList[0]||{}
},
3.2 属性值[排他操作]
<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" @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">
{{ spuSaleAttrValue.saleAttrValueName }}
</dd>
</dl>
changeActive(SaleAttrValue,arr){
console.log(SaleAttrValue,arr);
arr.forEach(item => {
item.isChecked=0
});
SaleAttrValue.isChecked=1
}
3.3轮播图【js渲染选中状态-同级组件传递(全局事件总线)】
<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>
import Swiper from "swiper";
export default {
name: "ImageList",
props: ["skuImageList"],
data() {
return{
currentIndex:0
}
},
watch: {
skuImageList() {
this.$nextTick(() => {
new Swiper(this.$refs.cur, {
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
//显示几个图片
slidesPerView: 2,
slidesPerGroup: 1,
});
});
},
},
methods:{
changeCurrentIndex(index){
this.currentIndex=index
//通知同级放大镜组件的index
this.$bus.$emit('getIndex',this.currentIndex)
},
},
};
mounted() {
this.$bus.$on("getIndex", (index) => {
this.currentIndex = index;
});
},
3.4 放大镜功能实现
<template>
<div class="spec-preview">
<img :src="imgObj.imgUrl" />
<div class="event" @mousemove="handler"></div>
<div class="big">
<img :src="imgObj.imgUrl" ref="big" />
</div>
<div class="mask" ref="mask"></div>
</div>
</template>
export default {
name: "Zoom",
props: ["skuImageList"],
data() {
return {
currentIndex: 0,
};
},
computed: {
imgObj() {
return this.skuImageList[this.currentIndex] || {};
},
},
mounted() {
this.$bus.$on("getIndex", (index) => {
this.currentIndex = index;
});
},
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
mask.style.left=left+'px';
mask.style.top=top+'px';
big.style.left=-2*left+'px'
big.style.top=-2*top+'px'
},
},
};
</script>
3.5 加入购物车-成功页面
加入购物车,有三步操作
//1.发请求-将产品加入到数据库(通知服务器)
//2.服务器存储成功,进行路由跳转传递参数
//3.失败:给用户进行提示
从详情页到添加到购物车成功页时,购物车成功页面也需要产品详情页的数据,可以通过请求获取数据,也可以用路由跳转,还需要把一些详情页中的商品参数传过去,也可以通过路由传参+会话存储实现。怎么办?
本地存储【local storage持久】;会话存储【session storage不持久,关闭页面就无】
因为买一件展示一件,没必要存起来,想看谁,展示谁即可。所以添加购物车成功页面,搜用会话存储。
1.路由跳转加传参:需要传的参数时{}对象形式,不是单一的数据,用query传参:但是这种格式地址栏中对象转为字符串格式,很丑,且如果是对象,新页面刷新后就获取不到了
this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
2.只用路由传个数,数据用会话存储
路由接收:在标签里:$route.query.skuNum,在js:this.$route.query.skuNum
本地存储和会话存储不允许存对像形式,可在application的session storage中查看,或打印看
存的时候:JSON.stringify(this.skuInfo)) //转为字符串形式
获取的时候:
//购物车页面
//sessionStorage.setItem('SKUINFO',this.skuInfo)//不对,接收的是对象[object Object]
sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))//转为字符串形式
//添加购物车后成功页面
mounted(){
// console.log(sessionStorage.getItem('SKUINFO'));//得到的是字符串形式,打印可以看出
console.log(JSON.parse(sessionStorage.getItem('SKUINFO')));//测试下
},
//可以在computed中直接计算得到
computed:{
skuInfo(){
return JSON.parse(sessionStorage.getItem('SKUINFO' ))//接收时转为对象形式
},
}
如此就在vue开发工具中查看到computed的skuInfo了
//添加购物车页
<div class="cartWrap">
<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>
<div class="add">
<a @click="addShopCar">加入购物车</a>
</div>
</div>
//加入购物车
async addShopCar(){
//1.发请求-将产品加入到数据库(通知服务器)
// let result=this.$store.dispatch('addOrUpadteShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})
// console.log(result,888);//返回的是promise对象
try {
//当前 派发一个action,下面的代码是调用仓库的addOrUpadteShopCart方法,这个方法加上async,返回的肯定是Promise,要么成功,
await this.$store.dispatch('addOrUpadteShopCart',{skuId:this.$route.params.skuid,skuNum:this.skuNum})
//1.成功后跳转路由,跳转及传参---地址栏对象转为字符串形式,很丑
// this.$router.push({name:'addCartSuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
//2.skuNum用路由传参,skuInfo用会话存储
// sessionStorage.setItem('SKUINFO',this.skuInfo)//不对,接收的是对象[object Object]
sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))//转为字符串形式
this.$router.push({name:'addCartSuccess',query:{skuNum:this.skuNum}})
} catch (error) {
alert(error.message)
}
//2.服务器存储成功,进行路由跳转传递参数
//3.失败:给用户进行提示
},
//直接计算出skuinfo
computed:{
skuInfo(){
// return sessionStorage.getItem('SKUINFO')//接收
return JSON.parse(sessionStorage.getItem('SKUINFO' ))//接收时转为对象形式
},
}
3.6 成功页面-返回查看商品详情
<router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link>
3.7 成功页面-去购物车结算
<router-link to="/ShopCart" >去购物车结算 > </router-link>
需要携带客户信息【uuid】,先用临时游客身份,一次登录游客身份,下次再次登录游客身份,应该是同一个,不会变,且永久保存。用local storage。
点击去加入购物车时,除了带产品id,个数,还需要带着游客什么身份信息,可以通过请求头携带,在请求拦截器中使用。
【浏览器npm中搜uuid,可按照说明说明和使用,因node_modules中有,所以不用再安装了】,还有nanoid
先从本地获取,没有的话,再函数生成
在src根目录下新建一个文件utils-》uuid_token.js,该文件中常放诸如【正则,游客身份等】常用功能。
这是个封装函数,要有返回值 return
import { v4 as uuidv4 } from 'uuid';
//生成一个随机数,每次执行不能发生变化,且游客身份持久保存
export const getUUID=()=>{
//先从本地存储中获取uuid,看是否有
let uuid_token=localStorage.getItem('UUIDTOKEN')//none
//没有的话
if(!uuid_token){
//生成游客临时身份
uuid_token=uuidv4()
//本地存储存储一次
localStorage.setItem('UUIDTOKEN',uuid_token)
}
return uuid_token;
}
存到store中,需要在仓库的detail.js中引入
//封装游客身份模块uuid,生成一个不能改变的随机数字
import {getUUID} from '@/utils/uuid_token'
const state={
//不要乱写,看接口文档返回数据类型,是{}
goodInfo:{},
//游客临时身份
uuid_token:getUUID()
};
然后放在请求头中,在api=>request.js中
不仅要存到store中,还需要带给服务器,在请求头中,这样每一个请求中都含有请求头—这个临时游客身份【userTempId】
//请求拦截器
requests.interceptors.request.use((config) => {
//config:配置对象,里面有header属性
if (store.state.detail.uuid_token) {
//请求头添加字段【userTempId】:固定的
config.headers.userTempId = store.state.detail.uuid_token;
}
nprogress.start();
return config;
});
购物车数据有了之后,渲染静态页面,另外计算每一个产品的数量,产品小计、总价和全选
//用的value
<input autocomplete="off" type="text" :value="cart.skuNum" minnum="1" class="itxt" />
<span class="sum">{{cart.skuPrice*cart.skuNum}}</span>
<input class="chooseAll" type="checkbox" :checked="isAllCheck" />
computed: {
//总计
totalPrice(){
let sum=0;
this.cartInfoList.forEach(item => {
sum+=item.skuPrice*item.skuNum
});
return sum
},
//全选
isAllCheck(){
return this.cartInfoList.every(item=>item.isChecked==1);
},
},
修改购物车产品数量(需发请求)
<a href="#none" class="sindelet" @click="deleteCartById(cart)">删除</a>
<li class="cart-list-con5">
<a
href="javascript:void(0)"
class="mins"
@click="handler('minus', -1, cart)"
>-</a
>
<input
autocomplete="off"
type="text"
:value="cart.skuNum"
minnum="1"
class="itxt"
@change="handler('change', $event.target.value * 1, cart)"
/>
<a
href="javascript:void(0)"
class="plus"
@click="handler('add', 1, cart)"
>+</a
>
</li>
const actions={
//将产品添加到购物车中
async addOrUpadteShopCart({commit},{skuId,skuNum}){
//加入购物车后(发请求),前台将参数带给服务器,服务器写入数据成功,并没有返回其他数据,知识返回code=200,表示操作成功
//因服务器并没有返回其他数据,因此不需要在vuex三连存储数据
let result=await reqAddOrUpdateShopCart(skuId,skuNum)
if(result.code==200){
//加入购物车成功
return 'ok'
}else{
//加入购物车失败
return Promise.reject(new Error('faile'))
}
},
};
handler: throttle(async function (type, disNum, cart) {
//type区分三个元素
//disNum变化量和最终量
//cart产品id
switch (type) {
case "add":
disNum = 1;
break;
case "minus":
disNum = cart.skuNum > 1 ? -1 : 0;
break;
case "change":
//输入非法【字母,汉字】或者负数
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//小数取整
disNum = parseInt(disNum) - cart.skuNum;
}
break;
}
// console.log(disNum);
//派发请求
try {
await this.$store.dispatch("addOrUpadteShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
this.getData();
} catch (error) {}
}, 800),
当快速增加或删除购物车产品数量时,出现负数【因为操作太快,导致后台数据还没回来,用节流】
删除某一产品
//按需引入,因为是默认暴露的,所以不加 { throttle }大括号了
import throttle from "lodash/throttle";
handler:throttle( async function(type, disNum, cart){
//type区分三个元素
//disNum变化量和最终量
//cart产品id
switch (type) {
case "add":
disNum=1;
break;
case "minus":
disNum=cart.skuNum>1?-1:0
break;
case "change":
//输入非法【字母,汉字】或者负数
if(isNaN(disNum)||disNum<1){
disNum=0
}else{
//小数取证
disNum=parseInt(disNum)-cart.skuNum
}
break;
}
// console.log(disNum);
//派发请求
try {
await this.$store.dispatch('addOrUpadteShopCart',{skuId:cart.skuId,skuNum:disNum})
this.getData()
} catch (error) {
}
},800),
修改产品状态-删除
const actions = {
//删除购物车商品
async deleteCartList({commit},skuId){
let result=await reqDeleteCartById(skuId)
if(result.code==200){
return 'ok'
}else{
return Promise.reject(new Error('faile'))
}
}
};
async deleteCartById(cart) {
try {
await this.$store.dispatch("deleteCartList", cart.skuId);
this.getData();
} catch (error) {
alert(error.message);
}
},
修改商品的勾选状态
全选按钮跟着变化
@change="updateChecked(cart, $event)"
//修改某产品选中状态
async updateChecked(cart, event) {
try {
console.log(event.target.checked,78778);
let isChecked = event.target.checked ? "1" : "0";
await this.$store.dispatch("updateCheckById", {
skuId: cart.skuId,
isChecked: isChecked,
});
this.getData()
} catch (error) {
alert(error.message);
}
},
删除选中的全部商品
写接口,vuex,渲染
接口和删除单个接口一致。
//删除所选商品 vuex的actions
// deleteAllCheckedCart(context){
// console.log(context);//打印出来的是一个小仓库
// },
deleteAllCheckedCart({ dispatch, getters }) {
let promiseAll=[]
getters.cartList.cartInfoList.forEach((item) => {
let promise=item.isChecked == 1 ? dispatch("deleteCartList", item.skuId) : "";
promiseAll.push(promise)
});
//只有p1,p2,p3...全部成功,返回结果即为成功
return Promise.all(promiseAll)
},
//删除选中的商品
async deleteAllCheckedCart(){
try {
await this.$store.dispatch('deleteAllCheckedCart')
this.getData()
} catch (error) {
alert(error.message)
}
},
全选按钮功能
同删除,注意没有数据时,全选没有被选中
<div class="select-all">
<input
class="chooseAll"
type="checkbox"
:checked="isAllCheck&&cartInfoList.length>1"
@change="updateAllChecked"
/>
<span>全选</span>
</div>
//全选状态
async updateAllChecked(event) {
try {
let isChecked = event.target.checked ? "1" : "0";
await this.$store.dispatch("updateAllChecked", { isChecked });
this.getData();
} catch (error) {
alert(error.message)
}
},
const actions = {
//切换产品选中状态-没有返回数据的,要返回一个成功或失败的结果
async updateCheckById({ commit }, { skuId, isChecked }) {
let result = await reqUpdateCheckById(skuId, isChecked);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
updateAllChecked({dispatch,state},{isChecked}){
let promiseAll=[]
state.cartList[0].cartInfoList.forEach((item)=>{
let promise=dispatch('updateCheckById',{skuId:item.skuId,isChecked})
promiseAll.push(promise)
})
return Promise.all(promiseAll)
},
};
结算(结算之前先解决登录和注册)–08
filter返回的是新数组
map返回的是新数组
find查找数组中符合条件的元素返回为最终结果
渲染:
提交订单成功
提交后,向服务发请求【支付信息】
不用vuex了。练练
api放到去全局的操作,所以所有组件内可以不引用接口,可直接使用
提交成功后返回订单号:进行路由跳转和传参【订单号】
支付:微信支付【√】,支付宝支付
生命周期尽量别加async:async mounted
点击支付,出现二维码支付
elementui
二维码
npm=>qrcode安装插件,应用
获取支付订单状态,要一直询问是否支付==》长连接
个人中心:
未完待续。。。。