目录
(一)详情页detail模块搭建
1.搭建静态页面
[1]静态页面搭建
[2]配置路由+路由模块化
2.获取详细商品信息
[1]配置发送axios请求和detail仓库
3.动态展示数据
[1]三级商品目录展示
[2]标题和价格的数据展示
[3]商品平台售卖属性的展示和选择
[4]实现购买商品数量的操作
4.放大镜和小轮播图组件的操作
[1] 小轮播图组件
[2]放大镜组件
[3]小轮播图和放大镜组件的数据传输
(二)加入购物车成功页面addCartSuccess搭建
1.发送商品id和数量的加入购物车请求
(1)配置加入购物车请求api和store
(2)商品成功加入购物车的路由组件
(3)完善加入购物车成功组件的功能
(三)购物车页面shopcart搭建
1.配置路由+静态组件
2.请求购物车数据
(1)配置接口+store仓库
(2)获取游客nanoid进行请求数据操作
3.动态展示购物车数据
4.实现购物车功能
(1)获取购物车商品数据
(2)单选并展示总价的功能
(3)更改商品数量的功能
(4)删除单个商品的功能
(5)单选商品修改选中状态的功能
(6)删除选中的商品的功能
(7)点击全选和反选的功能
用户在搜索页点击商品图片就会跳转到商品详情页,商品信息根据商品id向服务器请求数据动态展示出
配置路由
routes:[
{
name: 'detail',
path: '/detail/:skuId', //params参数
component: Detail,
meta: { showFooter: true }
},
]
将routes配置项移到routes.js实现模块化管理
将routes数组写到routes.js中并默认暴露,index.js直接引用即可
在router/index中:
// 引入路由配置
import routes from "./routes";
const router = new VueRouter({
routes,
// 路由跳转后回到网页最上方
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
})
export default router
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 滚动行为 | Vue Router
使用声明式导航实现路由跳转
传递商品skuId 以params参数形式传递
在pages/search/index中:
在api/index中:
// 获取详情商品数据 get请求 携带参数skuId
export const goodsInfo = (skuId) => requests.get(`/item/${skuId}`)
在store/Detail/index中:
// 引入axios goodsInfo
import { goodsInfo } from "@/api"
// 用于异步操作 不进行数据操作
const actions = {
// 发送请求获取categorylist数据
async getGoodsInfo(context, skuId) {
const result = await goodsInfo(skuId) //axios返回promise实例
if (result.code == 200) { //成功接收
console.log(result.data);
context.commit('GETGOODSINFO', result)
}
},
}
// 进行数据操作
const mutations = {
// 更新state中bannerList的值
GETGOODSINFO(state, result) {
state.goodsInfo = result.data
},
}
// 存储共享数据
const state = {
goodsInfo: {}
}
向服务器/api/item发送get请求,携带参数skuId,返回的数据是一个对象
在pages/detail/index中:
mounted() {
// 发送axios请求数据
this.$store.dispatch('getGoodsInfo', this.skuId)
},
computed: {
skuId() {
return this.$route.params.skuId
}
}
将获取到的详细商品数据在组件上动态展示
在store/detail中:
const getters = {
// 一二三级商品目录
categoryView(state) {
return state.goodsInfo.categoryView || {}
},
}
在pages/detail/index中:
{{ categoryView.category1Name }}
{{ categoryView.category2Name }}
{{ categoryView.category3Name }}
computed:{
...mapGetters(['categoryView'])
}
实现点击对应一二级目录跳转到对应的搜素页
methods: {
// 点击一二三级标签跳转到对应路由
gotoCategory(num) {
let location = { name: 'search' }
if (num == 1) {
location.query = {
categoryName: this.categoryView.category1Name,
category1Id: this.categoryView.category1Id
}
} else if (num == 2) {
location.query = {
categoryName: this.categoryView.category1Name,
category2Id: this.categoryView.category2Id
}
} else {
location.query = {
categoryName: this.categoryView.category1Name,
category3Id: this.categoryView.category3Id
}
}
this.$router.push(location)
},
}
在store/detail中:
const getters = {
// 商品信息
skuInfo(state) {
return state.goodsInfo.skuInfo || {}
},
}
在pages/detail/index中:
{{ skuInfo.skuName }}
¥
{{ skuInfo.price }}
降价通知
computed:{
...mapGetters(['skuInfo'])
}
展示获取到的商品数据
从服务器获取到的数据中每个售卖属性有固定的isChecked的值
在store/detail中:
const getters = {
// 商品平台售卖信息
spuSaleAttrList(state) {
return state.goodsInfo.spuSaleAttrList || []
}
}
在pages/detail/index中:
- {{ spuSaleAttr.saleAttrName }}
- {{ value.saleAttrValueName }}
computed:{
...mapGetters(['spuSaleAttrList'])
}
实现点击对应标签显示对应标签的active
{{ value.saleAttrValueName }}
methods:{
changeChecked(value, spuSaleAttrValueList) {
// 先将所有选项都改为0
spuSaleAttrValueList.forEach(item => {
item.isChecked = 0
})
// 再将点击的目标选项改为1
value.isChecked = 1
},
}
默认数量为1,点击加减进行数量的改变,可直接在input框内输入数量,对输入的内容要进行判断
methods:{
changeNum(e) {
//通过event事件对象获取用户输入内容[用户输入的内容一定是字符串类型的数据]
let value = e.target.value * 1;
//用户输入进来非法情况判断
if (isNaN(value) || value < 1) { //非数字乘以1会变成NaN,可用于判断是否是数字
this.skuNum = 1;
} else {
//正常情况
this.skuNum = parseInt(value);
}
},
}
图片数据父传子
在pages/detail/index中:
computed: {
// 给子组件的图片信息数据 若还没获取到就返还空数组
skuImageList() {
return this.skuInfo.skuImageList || []
}
},
利用swiper插件将图片数据展示出
在pages/detail/imageList中:
// 局部引入swiper插件
import { Swiper, SwiperSlide } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: "ImageList",
data() {
return {
// 默认第一张图片 active样式
imgIndex: 0,
// 轮播图配置
swiperOptions: {
slidesPerView: 6,
// spaceBetween: 10,
//导航前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
}
}
},
components: {
Swiper, SwiperSlide
},
props: ['skuImageList'],
图片数据父传子
在pages/detail/index中:
先将第一张图片展示在放大镜上,后面再根据小轮播图组件的操作动态改变放大镜展示的图片
computed: {
image() {
return this.skuImageList[0] || {}
}
},
props: ['skuImageList'],
用户鼠标移入小轮播图组件中的图片时向放大镜组件发送移入的图片的数据,从而实现同步展示
在pages/detail/imageList中:
methods: {
changeImage(index) {
// 当鼠标进入的不是当前展示的图片时,就向zoom发送新的图片索引
if (this.imgIndex != index) {
this.imgIndex = index
// 全局事件总线 向兄弟组件发送图片索引
this.$bus.$emit('getImage', index)
}
}
},
在pages/detail/zoom中:
data() {
return {
// 索引默认为0
index: 0
}
},
mounted() {
// 接收imageList实时发来的图片索引
this.$bus.$on('getImage', (index) => {
this.index = index
})
},
beforeDestroy() {
this.$bus.$off('getImage')
},
computed: {
image() {
return this.skuImageList[this.index] || {}
}
},
实现用户鼠标移入放大镜实时展示放大区域
methods: {
move(e) {
//获取蒙板
let mask = this.$refs.mask;
let big = this.$refs.big;
//计算蒙板的left|top数值
let left = e.offsetX - mask.offsetWidth / 2;
let top = e.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";
},
},
向/api/cart/addToCart/{ skuId }/{ skuNum }端口发送请求(在路径中传递数据)
在api/index中:
// 将加入购物车的商品信息(或要修改的商品数量信息)传入服务器中 post请求,发送/{skuId}/{skuNum}
export const addUpdateShopcar = (skuId, skuNum) => requests({
url: `/cart/addToCart/${skuId}/${skuNum}`,
method: 'post',
})
在store/detaill中:
const actions = {
// 发送请求添加或修改购物车商品信息 store读取的参数只能是单个
// async函数返回的一定是一个promise对象
async addUpdateShopcar(context, { skuId, skuNum }) {
const result = await addUpdateShopcar(skuId, skuNum)
// 加入购物车成功
if (result.code == 200) {
return 'ok'
} else { //加入购物车失败
return Promise.reject(new Error('加入购物车失败'))
}
}
}
在detail组件中调用$store的addUpdateShopcar本质是返回了一个promise函数,所以使用then接收是否加入购物车成功的结果即可
在pages/detail/index中:
加入购物车
methods:{
// 发送请求将产品加入到购物车中
addShopcar() {
// 发送请求
this.$store.dispatch('addUpdateShopcar', {
skuId: this.skuId, skuNum: this.skuNum
}).then((resolve) => { // 返回成功
// 进行路由跳转 跳转到购物车界面
this.$router.push({
path: '/addcartsuccess',
query: { // 将该商品的信息和数量传递给添加购物车路由
skuInfo: this.skuInfo,
skuNum: this.skuNum
}
})
}, (reject) => { // 返回失败
alert(reject)
})
}
}
这里使用了query参数传递数据,但会跳转后地址栏看起来很杂乱,另一种方法是采用会话存储sessionStorage将数据保存在本地,跳转到成功加入购物车的路由组件时再获取数据也是可行的。
配置路由
在router/routes中:
{
name: 'addcartsuccess',
path: '/addcartsuccess',
component: AddCartSuccess,
meta: { showFooter: true, showInput: false }
},
静态组件结构:
...
动态展示数据
{{ skuData.skuInfo.skuName }}
数量:{{ skuData.skuNum }}
查看商品详情
这里也可以使用push带query参数跳转到对应的详情页,用back应该算是偷懒了
查看商品详情
去购物车结算
去购物车结算 >
配置路由
在router/routes中:
{
name: 'shopcart',
path: '/shopcart',
component: ShopCart,
meta: { showFooter: true, showInput: false }
},
静态组件
向/api/cart/cartList端口请求数据
注意:购物车数据不能直接请求到,必须发送自定义请求头userTempId才可以请求到对应的购物车数据
在api/index中:
// 获取购物车数据 get请求
export const cartList = () => requests.get('/cart/cartList')
在store/shopcart中:
const actions = {
async getCartList(context) {
const result = await cartList()
if (result.code == 200) {
context.commit('GETCARTLIST', result)
}
}
}
// 进行数据操作
const mutations = {
GETCARTLIST(state, result) {
state.cartData = result[0] || []
}
}
// 用于加工state内的数据 类似于computed
const getters = {
cartList(state) {
return state.cartData.cartInfoList || []
}
}
// 存储共享数据
const state = {
cartData: [],
}
目前还没有搞到用户登录注册的业务,所以先使用一个游客id进行练习,同时这个id要保持在网页中不变,所以使用nanoid插件随机一个id来使用
创建utils文件夹(用于存放一些工具函数,如正则表达式等)
在utils/nanoid_token中:
// 生成唯一的游客id
import { nanoid } from "nanoid"
export const nanoid_token = () => {
// 在本地存储中寻找有没有游客id
let nanoId = localStorage.getItem('nanoid_token')
// 没有就新建一个
if (!nanoId) {
nanoId = nanoid()
localStorage.setItem('nanoid_token', nanoId)
}
return nanoId
}
shopCart组件获取nanoid(保存在store中)
在store/shopcart中:
// 引入游客id nanoid_token
import { nanoid_token } from "@/utils/nanoid_token"
// 存储共享数据
const state = {
nanoid_token: nanoid_token()
}
将nanoid添加到请求头中:
在api/ajax中:
// 引入游客id
import store from "@/store"
// 添加请求拦截器
requests.interceptors.request.use(
(config) => {
// 这里写发送请求后的操作
// 如果有游客id就将这个id写在header里带给服务器 会返回对应的id的购物车信息
if (store.state.ShopCart.nanoid_token) {
config.headers.userTempId = store.state.ShopCart.nanoid_token
}
...
},
(err) => {...}
)
把getters获取到的cartList数据展示到页面上(名字、图片、价格、总价、数量)
在pages/shopcart中:
-
{{ cart.skuName }}
-
{{ cart.skuPrice }}
-
+
-
{{ cart.skuPrice * cart.skuNum }}
computed: {
...mapGetters(['cartList']),
}
写成函数方便复用;初次挂载时就发请求加载数据
methods: {
// 获取购物车数据
getData() {
this.$store.dispatch('getCartList')
},
},
mounted() {
// 请求获取购物车数据
this.getData()
},
用计算属性实时判断cartList中商品的isChecked状态
已选择
{{ total.sumNum }}件商品
总价(不含运费) :
{{ total.sumPrice }}
computed:{
// 计算选中商品的总价和数量
total() {
let sumPrice = 0
let sumNum = 0
this.cartList.forEach((item) => {
if (item.isChecked) {
sumPrice += item.skuPrice * item.skuNum
sumNum += item.skuNum
}
})
return { sumNum, sumPrice }
},
}
点击加减按钮进行增减,也可以直接输入数量(要判断输入是否合法)
配置请求更改商品数量的store
修改商品数量的api已经引入,直接引用就行
在store/shopcart中:
// 引入添加或修改购物车数量
import { addUpdateShopcar } from "@/api"
const actions = {
// 发送请求修改购物车商品信息 store读取的参数只能是单个;skuNum为原来数量的加减(-1 3等)
// async函数返回的一定是一个promise对象
async UpdateShopcar(context, { skuId, skuNum }) {
const result = await addUpdateShopcar(skuId, skuNum)
// 修改数量成功
if (result.code == 200) {
return 'ok'
} else { //修改数量失败
return Promise.reject(new Error('失败!'))
}
},
}
实现点击或输入修改数量
以加减形式修改向函数传入标志0;以输入框形式修改传入标志1
-
+
methods:{
// num表示修改后数量和修改前数量的差值;tag=0表示按钮操作,tag=1表示输入操作;cart表示当前操作的商品的数据
changeSkuNum(num, tag, cart) {
// 如果tag=0就是按钮操作, 并判断操作后的商品数量是否大于0
// 如果tag=1,判断输入框内容是否为数字,以及输入的数量是否大于0
if (!tag && cart.skuNum + num > 0 || tag && !isNaN(num) && num > -cart.skuNum) {
num = parseInt(num)
} else { // 如果输入非法的内容会发送更改为0的请求
num = 0
}
// 发送请求
this.$store.dispatch('UpdateShopcar', {
skuId: cart.skuId,
skuNum: num
}).then(res => {
// 重新请求获取购物车数据
this.getData()
}, (err) => { alert(err); })
},
}
配置请求删除商品的api和store
在api/index中:
// 通过skuId删除购物车商品数据 delete请求
export const deleteCart = (skuId) => requests({
url: `/cart/deleteCart/${skuId}`,
method: 'delete'
})
在store/shopcart中:
const actions = {
// 发送请求删除skuId商品的数据
async deleteCartInfo(context, skuId) {
const result = await deleteCart(skuId)
if (result.code == 200) {
return 'ok';
} else {
return Promise.reject();
}
},
}
点击删除按钮执行删除单个商品函数
在pages/shopcart中:
删除
methods:{
// 发送请求删除对应的商品数据
deleteCartById(skuId) {
this.$store.dispatch('deleteCartInfo', skuId).then(
res => {
// 重新请求获取购物车数据
this.getData()
}, (err) => { alert(err); })
},
}
点击商品的选择框实时更新服务器中商品的isChecked属性
配置请求修改商品isChecked属性的api和store
在api/index中:
// 修改商品的选中状态 get请求 传递参数/cart/checkCart/{skuID}/{isChecked}
export const changeCartChecked = (skuId, isChecked) => requests({
url: `/cart/checkCart/${skuId}/${isChecked}`,
method: 'get'
})
在store/shopcart中:
const actions = {
// 发送请求修改商品的选中状态isChecked
async changeChecked(context, { skuId, isChecked }) {
let result = await changeCartChecked(skuId, isChecked)
// 修改选中状态成功
if (result.code == 200) {
return 'ok'
} else { // 修改选中状态失败
console.log(result);
return Promise.reject(new Error('失败!'))
}
},
}
点击选择框触发改变isChecked函数
在pages/shopcart中:
methods:{
// 点击商品复选框修改isChecked状态
changeChecked(cart) {
// 发送修改请求
this.$store.dispatch('changeChecked', {
skuId: cart.skuId,
// 如果目前复选框选中则目标提交修改状态为0,否则为1
isChecked: cart.isChecked ? '0' : '1'
}).then(
(res) => { //成功就重新读取数据
this.getData()
}, (err) => { // 失败就输出错误
alert(err)
})
},
}
删除选中商品需要实时传递商品的isChecked参数
思路:遍历store里的商品,isChecked参数为真则执行删除单个商品的actions
在pages/shopcart中:
删除选中的商品
methods:{
// 删除所有选中的商品
deleteAllChecked() {
this.$store.dispatch('deleteAllChecked').then(
(res) => {
this.getData()
}, (err) => { alert(err) })
},
}
在store/shopcart中:
const actions = {
// 删除所有选中状态的商品
deleteAllChecked({ getters, dispatch }) {
let promises = []
getters.cartList.forEach(item => {
if (item.isChecked == 1) {
// 删除选中的元素
let p = dispatch('deleteCartInfo', item.skuId) //每次返回一个promise对象
promises.push(p) //将单个promise存放进数组中
}
})
// Promise.all([p1,p2,...])只要有一个promise对象rejected那么整个promise数组都是rejected
return Promise.all(promises)
},
}
Promise.all()用法
(1)只有数组中所有promise的状态都变成
fulfilled
,promises的状态才会变成fulfilled
,此时返回值组成一个数组,传递给promises的回调函数。(2)只要数组之中有一个被
rejected
,promises的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给promises的回调函数。
此处也同理,只需要判断全选框的状态再传给store函数就可以进行isChecked的修改
再用计算属性实时判断全选状态
在pages/shopcart中:
computed:{
// 判断是否全选
isAllChecked() {
// 数组方法every:每个子项都满足条件时返回真
let checked = this.cartList.every(item => item.isChecked == true)
return checked
}
}
methods:{
// 点击全选或反选
allChecked(e) {
let isChecked = e.target.checked ? '1' : '0'
this.$store.dispatch('clickAllChecked', isChecked).then((res) => {
this.getData()
}, err => {
alert(err.message)
})
},
}
在store/shopcart中:
const actions = {
// 点击全选或反选,传入isChecked为真说明是正选,反之是反选;和删除所有选中商品的思路一致
clickAllChecked({ getters, dispatch }, isChecked) {
let promises = []
getters.cartList.forEach(item => {
if (item.isChecked != isChecked) {
// 和全选复选框状态不一致则修改
let p = dispatch('changeChecked', {
skuId: item.skuId,
isChecked
})
promises.push(p) // 将小promise对象存放到数组中
}
})
return Promise.all(promises)
},
}