Vue3小兔鲜 - 下

登录

静态页面搭建,并给首页的请求登录绑定跳转

<li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li>

表单校验

当使用UI组件库中提供的Form表单的时候,会提供表单的校验功能。根据文档设计需要的校验规则即可。

制定登录表单的校验规则

如下数据制定后绑定到表单中可以正常显示数据

const loginFormRef = ref()
const loginForm = reactive({
  account: "xiaotuxian001",
  password: "123456",
})
const loginRules = reactive({
  account: [
    { required: true, message: "用户名不能为空", trigger: "blur" }
  ],
  password: [
    // 双校验
    { required: true, message: "密码不能为空", trigger: "blur" },
    { min: 6, max: 14, message: "长度在 6 到 14 个字符", trigger: "blur" }
  ]
})

自定义校验规则

在Element plus表单组件中内置了一些简单的校验规则,如上面的代码就是。像这种简单的校验只需要调用提供的配置项即可。如果想定制一些特殊的校验规则就需要采用自定义校验规则,其有对应的验证格式
自定义校验规则依旧是在表单的校验对象中完成。添加一个新的字段,负责处理勾选时候的逻辑同时绑定自定义校验规则。

自定义校验的基本格式如下:

{
	validator:(rule,value,callback)=>{
		//callback不论成功或失败都需要调用
	}
}
const loginForm = reactive({
   ......
  agree: true
})
const loginRules = reactive({
  .........
  agree: [
    {
      // 自定义校验规则
      validator: (rule, value, callback) => {
        // value 每次勾选为true,否则false
        // callback 勾选通过的回调函数,否则调用打印错误提示
        if (value) {
          callback()
        } else {
          callback(new Error("请同意用户协议!"))
        }
      }
    }
  ]
})
 <el-form-item prop="agree" label-width="22px">
       <el-checkbox v-model="loginForm.agree" size="large">
         我已同意隐私条款和服务条款
       </el-checkbox>
</el-form-item>

在这里插入图片描述

登录校验

这里创建一个点击登录进行统一检验的函数,防止用户不输入内容直接登录,在函数内部,调用表单提供的validate函数,该函数传入一个回调函数并且配置参数,在回调函数内部会对所有的表单内容进行校验是否通过。全部通过则valid为true,这个时候就可以在true的逻辑代码片段中实现登录的主体功能了。

// 登录方法
const login = () => {
  // 调用表单提供的函数进行统一验证
  loginFormRef.value.validate((valid) => {
    console.log(valid)
  })
}

以下是基本的结构代码结合了校验属性

<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-position="right" label-width="60px"
              status-icon>
              <el-form-item prop="account" label="账户">
                <el-input v-model="loginForm.account" />
              </el-form-item>
              <el-form-item prop="password" label="密码">
                <el-input v-model="loginForm.password" />
              </el-form-item>
              <el-form-item prop="agree" label-width="22px">
                <el-checkbox v-model="loginForm.agree" size="large">
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
              <el-button size="large" class="subBtn" @click="login">点击登录</el-button>
</el-form>

实现登录与服务器验证功能

整个登录的基本逻辑如下,最外层if (valid)...else判断用户是否输入了数据,内层使用了try..catch进行报错捕获,即用户输入不正确信息的时候,将服务器返回的数据提示给用户看。同时在路由拦截器中做了一个统一错误提示。用户登录正确的时候,清空输入的内容,跳转至主页。

//封装的用户登录接口
export function loginAPI({ account, password }) {
  return instance({
    url: "/login",
    method: "POST",
    data: {
      account,
      password
    }
  })
}
const router = useRouter()
// 登录方法
const login = () => {
  const { account, password } = loginForm
  // 调用表单提供的函数进行统一验证
  loginFormRef.value.validate(async (valid) => {
    if (valid) {
      try {
        // 验证通过不报错
        await loginAPI({ account, password })
        ElMessage({
          message: '登录成功',
          type: 'success',
        })
        loginForm.account = ''
        loginForm.password = ''
        router.replace("/") //登录成功路由跳转
      } catch (e) {
        loginForm.account = ''
        loginForm.password = ''
      }
    } else {
      ElMessage({
        message: '请输入用户名和密码',
        type: 'error',
      })
    }
  })
}
// 添加响应拦截器
instance.interceptors.response.use(res => {
  return res
}, e => {
  ElMessage({
    type: 'warning',
    message: e.response.data.message
  })
  return Promise.reject(e)
});

pinia管理用户数据

封装一个stores/userStore.js管理用户的store数据,这里采用对象式,也可以使用函数式返回数据使用

import { defineStore } from "pinia";
import { loginAPI } from '@/apis/use';

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      userInfo: {} //proxy响应式对象
    }
  },
  actions: {
    async getUserInfo({ account, password }) {
      let res = await loginAPI({ account, password })
      this.userInfo = res
    }
  }
})

在这里调用封装在pinia中的接口去请求数据,使用await等待后面的语句执行完毕后,才会往下执行

try {
        // 验证通过不报错
        // await loginAPI({ account, password })
    await useUser.getUserInfo({ account, password })
}

这边验证,可以实现逻辑功能。

pinia用户数据持久化

引入第三方库实现持久化

这里采用插件帮助我们快速实现持久化效果。使用npm安装:npm i pinia-plugin-persistedstate
在入口文件只引入该插件注册使用

// 持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const app = createApp(App)
// 注册并使用插件
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

app.use(pinia)

这里采用的是选项式配置store,因此直接在对象内部配置:persist: true,即可每次操作state中的数据自动进行本地存储器

export const useUserStore = defineStore('user', {
.....
  persist: true,
})

登录成功后本地存储中出现登录数据即可。
在这里插入图片描述

自定义插件实现数据持久化

首先建立一个persistencePlugin.js文件,该代码的主要功能:针对userStore进行数据持久化处理。$subscribe()方法监听store状态的变化,即state数据每次变化的时候都会执行一次。mutation参数中存放不同Store的信息,可以根据信息只针对某一个store进行处理。state是当前store中的state数据。

export function persistencePlugin(ctx) {
  ctx.store.$subscribe((store, state) => {
    if (store.storeId === 'user') { //每一个store的唯一id
      localStorage.setItem(store.storeId, JSON.stringify(state.userInfo))
    }
  }, {
    detached: true //组件被卸载时,它将不被自动清理掉
  })
}

useUserStore.js文件中新增修改state中userInfo的方法。并在Layout/Index.vue文件的生命周期函数中引入并调用useUser.updateUserInfo()。这样子每次刷新的时候,pinia中数据不丢失。

    updateUserInfo() {
      this.userInfo = JSON.parse(localStorage.getItem('user') || "{}")
    }

在入口文件中引入并使用接口

import { persistencePlugin } from './stores/plugin/persistence'
const pinia = createPinia()
pinia.use(persistencePlugin)

这样子也能实现数据保存至本地存储中
在这里插入图片描述

登录和非登录模块切换

控制如图所示区域登录或非登录状态下显示正确的内容
在这里插入图片描述
LayoutNav组件中修改部分代码

import { useUserStore } from '@/stores/userStore';
const useUser = useUserStore()

验证该store中的数据字段是否存在token。同时修改名字字段显示正确的内容

<template v-if="useUser.userInfo.token">....</template> ....

统一携带token

如果一些服务器的安全系数很高,要求每次访问的时候都需要带上token,如果页面中多个接口都需要携带token,就可以使用拦截器配置。
在请求拦截器中配置每次请求的时候都带上token

  let token = JSON.parse(localStorage.getItem('user')).token //可能为undefined
  // 每次请求之前带上token
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }

退出登录

给退出登录气泡窗绑定UI组件库提供的确认事件 @confirm="handleConfirm"
主体代码如下,退出的时候删除对应store中的数据,同时情况本地存储,最后实现跳转。

   //useUserStore中新增的删除方法
    deleteUserInfo() {
      this.userInfo = {}
    }

在这里可以不主动删除本地存储,因为在插件中间接性的完成了。插件代码如下,每次退出登录到登录页面的时候,都会经过useUserStore()重新注册该store,而监视store改变的时候成功进入if判断语句中,而当前useUserStore的state中的userInfo数据为空,所以会同步更新到本地存储中。

const handleConfirm = () => {
  // 情况本地存储中的数据和pinia中的数据
  // localStorage.removeItem("user")
  useUser.deleteUserInfo()
  router.push('/login')
}

在这里插入图片描述

token失效处理

在网页中token是具有时效期的,如果过期就了就需要重新登录获取新的token保存。在所有的页面中都需要验证token的时效性,这个时候就可以使用拦截器了,在拦截器中的请求拦截器中每次将token发送给服务器验证,如果token失效了,就会返回401状态码被响应拦截器接收处理。

在响应拦截器中处理逻辑代码如下,先引入需要的文件,这里需要重点了解vue3中非vue文件中引入路由的方式如下 import router from "@/router";

import { useUserStore } from "@/stores/userStore";
import router from "@/router";
// 添加响应拦截器
instance.interceptors.response.use(res => {
  return res
}, e => {
  const userStore = useUserStore()
  ElMessage({
    type: 'warning',
    message: e.response.data.message
  })
  // 处理token失效问题
  if (e.response.status === 401) {
    userStore.deleteUserInfo() //删除store中的数据,然后跳转到登录页面
    router.push('/login') //跳转的同时会同步删除本地存储
  }
  return Promise.reject(e)
});

购物车

购物车中的数据需要进行持久化处理,下图是整体逻辑
在这里插入图片描述

本地购物车

以下是未登录情况下,按钮点击添加商品加入购物车的逻辑如下
在这里插入图片描述
首先创建一个skuObj变量保存每次返回的商品规格信息

let skuObj = {}
// 处理sku
const handleChange = (data) => {
  skuObj = data //每一个sku都具有唯一id,若空则无
}

引入增减计数组件,实现商品的增加和减少,在这里先不进行赋值,只进行简单的处理

// 增减商品数量
const count = ref(1)
// 数量改变时的回调
const handleCount = (value) => {
  //value为修改后的数量
  console.log(value)
}
<el-input-number v-model="count" @change="handleCount" />

在这里插入图片描述
其次每次点击最终的加入购物车按钮都需要进行sku验证。

<el-button size="large" class="btn" @click="addCart">加入购物车</el-button>
const useCart = useCartStore()
// 添加购物车
const addCart = () => {
  // 是否能够添加购物车依据sku中的id号
  if (skuObj.skuId) { //未全部选择商品规格,则为undefined
    // 添加购物车
    useCart.addCart({
      id: goods.value.id, //商品id
      name: goods.value.name, //商品名称
      picture: goods.value.mainPictures[0], //商品主图
      price: goods.value.price, //商品价格
      count: count.value, //商品数量
      skuId: skuObj.skuId, //商品规格
      attrsText: skuObj.specsText, //商品规格文本
      selected: true //是否选中
    })
  } else {
    // 未选择商品规格,不通过提示用户
    ElMessage({
      message: "请选择规格",
      type: "warning"
    })
  }
}

创建一个useCart文件,在该store中采用函数式的写法,其中updateCart 是为了每次输出的时候保持本地存储中数据和pinia中数据一致,在Layout/Index.vue中调用。

import { defineStore } from "pinia";
import { ref } from "vue";
export const useCartStore = defineStore("cart", () => {
  // 定义state
  const cartList = ref([])
  // 定义actions
  const addCart = (goods) => {
    // 首先查看当前已有商品中是否存在该商品
    // find方法返回数组中第一个满足条件的元素
    const item = cartList.value.find(item => item.skuId === goods.skuId)
    if (item) { 
      // 找到的情况下,同商品数量+1
      item.count++ //这里的item会同步到carList中对应的item项
    } else {
      // 没有找到的情况下,直接添加到数组中
      cartList.value.push(goods)
    }
  }
  
  // 更新本地存储数据到pinia
  const updateCart = () => {
    cartList.value = JSON.parse(localStorage.getItem('cart') || '[]')
  }
  return {
    cartList,
    addCart,
    updateCart 
  }
})

最后在持久化的插件中引入该代码,实现持久化效果

    if (store.storeId === 'cart') {
      localStorage.setItem(store.storeId, JSON.stringify(state.cartList))
    }

头部购物车图标逻辑代码实现

默认情况将商品的数量显示在右上角,每次点击进去的时候查看商品的规格信息。
在这里插入图片描述
创建一个文件,Layout/HeaderCart.vue,并在LayoutHeader.vue组件中引入使用<HeaderCart></HeaderCart>
HeaderCart.vue文件中代码如下,首先引入对应的store并使用,然后将数据遍历显示

// 读取pinia中商品的数量显示
const useCart = useCartStore()
  <div class="cart">
    <a class="curr" href="javascript:;">
      <i class="iconfont icon-cart"></i><em>{{ useCart.cartList.length }}</em>
    </a>
    <div class="layer">
      <div class="list">
        <div class="item" v-for="i in useCart.cartList" :key="i">
          <RouterLink to="">
            <img :src="i.picture" alt="" />
            <div class="center">
              <p class="name ellipsis-2">
                {{ i.name }}
              </p>
              <p class="attr ellipsis">{{ i.attrsText }}</p>
            </div>
            <div class="right">
              <p class="price">&yen;{{ i.price }}</p>
              <p class="count">x{{ i.count }}</p>
            </div>
          </RouterLink>
          <i class="iconfont icon-close-new" @click="store.delCart(i.skuId)"></i>
        </div>
      </div>
      <div class="foot">
        <div class="total">
          <p>共 10 件商品</p>
          <p>&yen; 100.00 </p>
        </div>
        <el-button size="large">去购物车结算</el-button>
      </div>
    </div>
  </div>

其中,最主要的控制商品显示的样式如下,默认情况下不显示,transform: translateY(-200px) scale(1, 0);该样式中scale(1, 0)控制容器缩放,不加translateY(-200px)的时候默认在容器中间进行上下缩放,因为scale(1, 0)中x轴为正常大小,y轴为0,代表横向大小正常,纵向被缩小了,且往中间缩放。这个时候translateY(-200px)可以造成视觉冲突从顶部下来的感觉,默认将位置移动到顶部,鼠标移入的时候将transform移除,进入过渡效果,一边恢复位置一边放大。

    opacity: 0;
    transition: all 0.4s 0.2s;
    transform: translateY(-200px) scale(1, 0);

扩展设置滚动条样式

    &::-webkit-scrollbar { //整个滚动条
      width: 10px;
      height: 10px;
    }

    &::-webkit-scrollbar-track { //滚动条轨道
      background: #f8f8f8;
      border-radius: 2px;
    }

    &::-webkit-scrollbar-thumb { //滚动条上的滚动滑块
      background: #eee;
      border-radius: 10px;
    }

    &::-webkit-scrollbar-thumb:hover {
      background: #ccc;
    }

在这里插入图片描述

删除购物车商品

cartStore中新增删除函数,这里使用filter函数过滤不需要的数据,同时需要知道该函数不会影响原数组,返回新数组,需要将新数组赋值过去

  // 删除购物车商品
  const delCart = (skuId) => {
    cartList.value = cartList.value.filter(item => item.skuId !== skuId)
  }

HeaderCart组件中使用该方法

<!-- 删除按钮 -->
<i class="iconfont icon-close-new" @click="useCart.delCart(i.skuId)"></i>

购物车统计模块

这里采用在pinia中使用计算属性来获取商品的总数量和总价使用。

  // cartStore中定义
  // 计算数量
  const allCount = computed(() => {
    return cartList.value.reduce((sum, item) => sum += item.count, 0)
  })
  // 计算总价
  const allPrice = computed(() => {
    return cartList.value.reduce((sum, item) => sum += item.count * item.price, 0)
  })
<p>共 {{ useCart.allCount }} 件商品</p>
<p>&yen; {{ useCart.allPrice.toFixed(2) }} </p>

在这里插入图片描述

列表购物车基本结构搭建

给上图所示的结算按钮绑定路由跳转

        {
          path: "cartList",
          component: () => import('@/views/CartList/Index.vue')
        }
<el-button size="large" @click="$router.push('/cartList')">去购物车结算</el-button>

创建一个CartList/Index.vue文件并编写基本结构代码,在代码中,对pinia中的商品数组cartList进行了判断,如果存在商品就显示,否则就显示一个空页面提示用户

列表购物车单选功能

需要将单选功能同步到pinia中的字段数据,保持两者之间同步。这里不直接使用v-model而是使用它的一般状态:model-value@change实现。读取pinia中的数据,并且在数据改变的时候能够即使同步到pinia中。

  1. 首先双向数据绑定,将pinia中对应商品的信息通过遍历绑定到复选框上显示
<el-checkbox :model-value="i.selected" @change="singleChecked(i, $event)" />

//也可以如下写法接收默认参数
@change="(selected) => singleCheck(i, selected)"
  1. 绑定一个change事件,并且需要在回调函数中传入两个参数,每次需要知道修改哪一个商品和修改的值是什么。其中el-checkbox组件提供了change事件,并且默认会提供一个参数,该参数为布尔值,决定了该复选框是否选择。如果即需要默认参数也需要使用自定义参数,那么就需要写出完整写法如上。在这里$event并事件对象,而是一个布尔值。
const singleChecked = (item, e) => {
  useCart.singleChecked(item.skuId, e) // e为true或false
}
  1. cartStore中新增修改复选框的代码
  //更新单选按钮状态
  const singleChecked = (skuId, selected) => {
    let item = cartList.value.find(item => item.skuId === skuId)
    item.selected = selected
  }

效果图如下
在这里插入图片描述

列表购物车全选功能

cartStore中新增如下方法

  //actions
  // 全选影响单选
  const allCheck = (selected) => {
    cartList.value.forEach(item => item.selected = selected)
  }

  //getter
  // 全选按钮(单选影响全选)
  const isAll = computed(() => {
    return cartList.value.every(item => item.selected === true)
  })
<el-checkbox :model-value="useCart.isAll" @change="allChecked" />
const allChecked = (selected) => {
  useCart.allCheck(selected)
}

购物车列表数据统计

cartStore中编写如下代码,最后在模板中使用即可

  // 统计已选数量
  const selectedCount = computed(() => {
    return cartList.value.filter(item => item.selected).reduce((sum, item) => {
      return sum += item.count
    }, 0)
  })
  // 统计已选数量的总价
  const selectedPrice = computed(() => {
    return cartList.value.filter(item => item.selected).reduce((sum, item) => {
      return sum += item.price * item.count
    }, 0)
  })

登录使用接口–购物车

当用户登录的时候,点击购物车添加的时候都会走接口请求。如何判断是否登录需要使用到token。
在这里插入图片描述

加入购物车

首先封装一个cart.jsAPI文件

// 加入购物车
export function insertCartAPI({ skuId, count }) {
  return instance({
    method: "POST",
    url: "/member/cart",
    data: {
      skuId,
      count
    }
  })
}

//获取最新购物车列表数据
export function getNewCartList() {
  return instance({
    method: "GET",
    url: "/member/cart"
  })
}

cartStore中引入userStore使用其中的token字段

  const useUser = useUserStore() //必须写在defineStore函数中
  const isHasToken = computed(() => useUser.userInfo.token) //判断是否存在数据
    // 添加购物车商品
  const addCart = async (goods) => {
    // 判断是否登录,进入不同的分支
    if (isHasToken.value) {
      // 登录状态,调用接口
      await insertCartAPI(goods)
      let res = await getNewCartList()
      cartList.value = res.data.result
    } else {
      // 没有登录
      // 首先查看当前已有商品中是否存在该商品
      // find方法返回数组中第一个满足条件的元素
      const item = cartList.value.find(item => item.skuId === goods.skuId)
      if (item) {
        // 找到的情况下,同商品数量+1
        item.count++
      } else {
        // 没有找到的情况下,直接添加到数组中
        cartList.value.push(goods)
      }
    }
  }

删除购物车

逻辑和加入购物车基本一致
在这里插入图片描述
封装一个删除数据接口

// 删除购物车商品
export function delCartAPI(ids) {
  return instance({
    method: "DELETE",
    url: "/member/cart",
    data: {
      ids
    }
  })
}
  // 删除购物车商品
  const delCart = async (skuId) => {
    if (isHasToken.value) {
      await delCartAPI([skuId]) //传入数组,参数支持一个或多个
      let res = await getNewCartList()
      cartList.value = res.data.result
    } else {
      cartList.value = cartList.value.filter(item => item.skuId !== skuId)
    }
  }

在这里会存在多个模块需要用到获取最新列表数据的接口,如加入和删除购物车,购物车全选或单选等。所以可以将公共的部分暂时分离出来使用。之后使用函数,替换掉重复代码的地方即可。

  const getCartList = async () => {
    let res = await getNewCartList()
    cartList.value = res.data.result
  }

退出登录–清空购物车

在之前的退出登录中,只清空了用户的数据,这次需要额外清空用户添加的商品信息(这里是清空本地存储的商品信息,非服务器)
cartStore中添加情况购物车数据的逻辑代码

  // 清除购物车数据
  const clearCart = () => {
    cartList.value = []
  }

userStore中代码如下

  state: () => {
    return {
.......
      useCart: useCartStore()
    }
  },

	//actions中代码
    deleteUserInfo() {
      this.userInfo = {} //清空用户数据
      this.useCart.clearCart() //清空用户添加的商品信息
    }

合并本地和登录状态下的购物车

如果未登录的时候用户在本地加入购物车,希望在登录后,将本地的数据与服务器购物车的数据进行合并。
封装一个合并购物车模块接口。用户需要在登录的时候调用该接口使用

// 合并购物车
export function mergeCartAPI(data) {
  return instance({
    method: "POST",
    url: "/member/cart/merge",
    data
  })
}

userStore的登录函数中添加如下代码


    async getUserInfo({ account, password }) {
      let res = await loginAPI({ account, password })
      this.userInfo = res.data.result
      // 合并购物车
      await mergeCartAPI(this.useCart.cartList.map(item => {
        return {
          skuId: item.skuId,
          selected: item.selected,
          count: item.count
        }
      }))
      // 调用最新列表数据显示
      this.useCart.getCartList()
    },

结算模块

路由配置和搭建结构

首先创建一个Checkout/index.vue组件并为其搭建路由信息

        {
          path: "checkout",
          component: () => import('@/views/Checkout/index.vue')
        }

之后在CartList/Index.vue组件中为按钮绑定点击跳转事件

<el-button class="btn" size="large" type="primary" @click="$router.push('/checkout')">下单结算</el-button>

封装一个接口文件,用于获取详情页订单数据/apis/checkout.js

export const getCheckoutInfoAPI = () => {
  return instance({
    method: "GET",
    url: '/member/order/pre'
  })
}

Checkout/index.vue文件中引入并使用

const checkInfo = ref({})  // 订单对象
const curAddress = ref({})  // 地址对象

//获取详情订单数据,包括了商品信息,用户地址信息等
const getCheckoutInfo = async () => {
  const res = await getCheckoutInfoAPI()
  checkInfo.value = res.data.result

  checkInfo.value.userAddresses.filter(item => {
    if (item.isDefault === 0) { //字段为0的默认显示
      curAddress.value = item
    }
  })
}

onMounted(() => {
  getCheckoutInfo()
})

在这里插入图片描述

地址切换

弹出层控制需要设置一个变量,并且在点击的时候使用v-model双向数据绑定该值

const isShow = ref(false) 
 <el-button size="large" @click="isShow = !isShow">切换地址</el-button>
 ......
 <el-dialog v-model="isShow" >....</el-dialog>

接下来需要处理地址激活的部分,首先如何判断激活的地址数据和当前地址数据是否一致(每一个地址信息都具有id),

  1. 创建一个激活地址变量,保存数据,方便比较
const activeAddress = ref({}) //当前激活对象
  1. 给每一个地址对象绑定点击事件,同时动态赋值active属性
<div class="text item" :class="{ active: activeAddress.id === item.id }" @click="switchAddress(item)">...</div>
// 激活的当前地址
const switchAddress = (item) => {
  activeAddress.value = item //保存当前点击激活的地点
}
  1. 添加初始状态绑定激活
    if (item.isDefault === 0) { //字段为0的默认显示
      curAddress.value = item
      activeAddress.value = curAddress.value //将默认的数据显示激活
    }
  1. 弹出层确定按钮,更换数据
<el-button type="primary" @click="confirm">确定</el-button>
// 地址弹出层确定按钮
const confirm = () => {
  // 显示的地址位激活的地址
  curAddress.value = activeAddress.value
  isShow.value = false //关闭弹出层
}

生成订单

首先准备基本模板。创建一个Pay/Index.vue文件,用于生成订单信息,每次跳转过来的时候路由地址如下,采用query形式传参,非params
在这里插入图片描述
创建对应的路由信息

        {
          path: "pay",
          component: () => import('@/views/Pay/Index.vue')
        }

创建对应的接口

export function createOrderAPI(data) {
  return instance({
    method: "POST",
    url: "/member/order",
    data
  })
}

Checkout/index,vue组件中为提交订单绑定点击事件,在该回调函数中调用接口传入参数,这里传参只需要注意goodsaddressId属性即可,其他均写默认值。并且根据返回的对象数据信息,取出生成订单的id数据进行路由跳转,并且每次跳转成功后,调用最新的购物车列表数据显示。

<el-button type="primary" size="large" @click="createOrder">提交订单</el-button>
const useCart = useCartStore()
const router = useRouter()

// 创建一个订单信息
const createOrder = async () => {
  let res = await createOrderAPI({
    deliveryTimeType: 1,
    payType: 1,
    payChannel: 1,
    buyerMessage: "",
    goods: checkInfo.value.goods.map(item => {
      return {
        skuId: item.skuId,
        count: item.count
      }
    }),
    addressId: curAddress.value.id
  })
  let orderId = res.data.result.id
  router.push({
    path: "/pay",
    query: {
      id: orderId
    }
  })
  useCart.getCartList()
}

支付模块

渲染基础数据

创建一个pay.js文件并编写代码

export function getOrderAPI(id) {
  return instance({
    method: 'GET',
    url: `/member/order/${id}`
  })
}

Pay/Index.vue文件中代码如下

const route = useRoute()
const payInfo = ref({})
// 获取订单详情信息
const getOreder = async () => {
  let res = await getOrderAPI(route.query.id)
  payInfo.value = res.data.result
  console.log(payInfo.value)
}
onMounted(() => {
  getOreder()
})

获取的输出结果如下,其中支付的限定时间为30分钟,如果countdown=-1则代表超时
在这里插入图片描述

实现支付功能

业务流程
在这里插入图片描述
Pay/Index.vue组件中设置支付宝的跳转地址,

// 支付地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/';
const backURL = 'http://127.0.0.1:5173/paycallback'; //支付成功后的自动跳转页面
const redirectUrl = encodeURIComponent(backURL);
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`;

支付结果展示

        {
          path: "paycallback",
          component: () => import('@/views/Pay/PayBack.vue')
        },

如果支付成功,跳转后的地址如下,根据payResultorderId属性的值渲染不同的内容
在这里插入图片描述
首先根据payResult的值只显示一个字体图标,同时修改对应显示的内容提示
在这里插入图片描述

        <span class="iconfont icon-queren2 green" v-if="$route.query.payResult === 'true'"></span>
        <span class="iconfont icon-shanchu red" v-else></span>
        <p class="tit">支付{{ $route.query.payResult === 'true' ? '成功' : '失败' }}</p>

之后根据路由参数中的的orderId获取当前订单的支付金额显示

let orderInfo = ref({});
let route = useRoute()
const gerOrderInfo = async () => {
  let res = await getOrderAPI(route.query.orderId);
  orderInfo.value = res.data.result
}

onMounted(() => gerOrderInfo())
<p>支付金额:<span>¥{{ orderInfo.payMoney?.toFixed(2) }}</span></p>

封装倒计时函数

将倒计时的函数封装在一个通用性的文件夹中使用,封装一个useCountDown.js文件

import { computed, onUnmounted, ref } from "vue";
import dayjs from "dayjs";

// 倒计时函数
export function useCountDown() {
  let timer = null
  const time = ref(0)
  // 格式化时间
  const formatTime = computed(() => {
    return dayjs.unix(time.value).format("mm分ss秒")
  })
  // 启动倒计时函数,参数为启动的时间
  const start = (currentTime) => {
    time.value = currentTime
    timer = setInterval(() => {
      if (time.value <= 0) return
      time.value--
    }, 1000)
  }

  // 组件卸载时候关闭定时器
  onUnmounted(() => {
    timer && clearInterval(timer)
  })
  return {
    formatTime,
    start
  }
}

Pay/Index.vue文件中引入并使用

const { formatTime, start } = useCountDown()
const getOreder = async () => {
.......
  start(res.data.result.countdown) //传入30分钟的时间戳
}

会员中心

路由搭建

搭建一个如下图所示的路由结构,在该模块中使用到了三级路由
在这里插入图片描述

        {
          path: "member",
          component: () => import('@/views/Member/Index.vue'),
          children: [
            {
              path: "user",
              component: () => import('@/views/Member/components/UserCenter.vue')
            },
            {
              path: "order",
              component: () => import('@/views/Member/components/UserOrder.vue')
            }
          ]
        }

个人中心信息展示

效果图如下
在这里插入图片描述
user.js文件中封装获取喜欢列表商品展示

// 封装猜你喜欢接口
export function getLikeListAPI(limit = 4) {
  return instance({
    method: "GET",
    url: "/goods/relevant",
    params: {
      limit
    }
  })
}

用户的信息展示可以直接读取pinia中存储的数据

const useUser = useUserStore()
const likeList = ref([])
const getLikeList = async () => {
  let res = await getLikeListAPI()
  likeList.value = res.data.result
}
onMounted(() => getLikeList())

我的订单列表渲染

封装一个oreder.js文件用于获取订单列表数据

export const getUserOrder = (params) => {
  return instance({
    url: '/member/order',
    method: 'GET',
    params
  })
}

UserOrder组件中代码如下

// 订单列表
const orderList = ref([])
// 接口参数
let params = ref({
  /* 订单状态,1为待付款、2为待发货、3为待收货、4为待评价、5为已完成、6为已取消,未传该参数或0为全部 */
  orderState: 0,
  page: 1,
  pageSize: 2
})
const getOrderList = async () => {
  let res = await getUserOrder(params.value)
  orderList.value = res.data.result.items
}
onMounted(() => getOrderList())

tab栏切换

点击不同的tab栏的时候需要修改对应的orderState的值,然后重新调用接口获取新数据显示。在tab栏UI组件中,提供了tab-change事件,在该事件的回调函数中默认提供了一个参数使用,该参数一一对应orderState状态的值,该参数对应当前元素在tab栏数组中的下标位置。

 <el-tabs @tab-change="tabChange">
	<el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" />
	。。。。。
  </el-tabs>
// tab列表
const tabTypes = [
  { name: "all", label: "全部订单" },
  { name: "unpay", label: "待付款" },
  { name: "deliver", label: "待发货" },
  { name: "receive", label: "待收货" },
  { name: "comment", label: "待评价" },
  { name: "complete", label: "已完成" },
  { name: "cancel", label: "已取消" }
]
// tab栏切换事件
const tabChange = (id) => {
  params.value.orderState = id
  getOrderList()
}

在这里插入图片描述
在这里插入图片描述

订单分页

使用Element plus提供的分页器组件完成,分页数=总的商品数量/每页数量。总的商品数量在初始化调用接口的时候就已经返回了。设置完分页数后,用户每次点击页数的时候都将当前页码获取修改params.page参数并重新发送网络请求。
在这里插入图片描述

const total = ref(0) //总的商品数量
const getOrderList = async () => {
.........
  total.value = res.data.result.counts
}
//默认page-size每页大小为10
<el-pagination 
	@current-change="pageChange" 
	:total="total" 
	:page-size="params.pageSize" 
	background layout="prev, pager, next"
/>

之后需要使用组件库提供的 current-change事件获取每次点击的页数

// 分页请求获取数据
const pageChange = (page) => {
  params.value.page = page
  getOrderList()
}

之后需要格式化页面中的部分数据显示,下图所示区域就需要进行状态适配
在这里插入图片描述

<p>{{ formatState(order.orderState) }}</p>
// 格式化状态显示
const formatState = (state) => {
  const stateLabel = {
    1: "待付款",
    2: "待发货",
    3: "待收货",
    4: "待评价",
    5: "已完成",
    6: "已取消"
  }
  return stateLabel[state]
}

sku组件

功能拆分

用户每次选择商品规格的时候,组件的激活状态都需要进行功能,如下图每次点击都会显示绿色边框。同时用户每次选择的时候还需要提示用户当前规格是否禁用,如下图中国字段。最后每次选择完成提交的时候都需要产出sku数据。
在这里插入图片描述

<template>
  <div class="goods-sku">
    <dl v-for="item in goods.specs" :key="item.id">
      <dt>{{ item.name }}</dt>
      <dd>
        <template v-for="val in item.values" :key="val.name">
          <!-- 图片类型规格 -->
          <img v-if="val.picture" :src="val.picture" :title="val.name">
          <!-- 文字类型规格 -->
          <span v-else>{{ val.name }}</span>
        </template>
      </dd>
    </dl>
  </div>
</template>
// 商品数据
const goods = ref({})
const getGoods = async () => {
  // 1135076  初始化就有无库存的规格
  // 1369155859933827074 更新之后有无库存项(蓝色-20cm-中国)
  const res = await axios.get('http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074')
  goods.value = res.data.result
}
onMounted(() => getGoods())

点击更新规格

  • 点击已选规格的时候,将该规格取消
  • 点击未选规格的时候,先将其他已选规格取消,在将自身规格选中
    在这里插入图片描述
    基本的逻辑代码如下
const changeSelectedStatus = (row, item) => {
  //row 一排的总商品规格信息
  // item 当前选中的规格
  if (item.selected) {
    //如果已选规格则取消
    item.selected = false
  } else {
    // 未选规格
    row.forEach(element => {
      element.selected = false //相当于给每一个对象添加新属性
    })
    item.selected = true
  }
}
 <!-- 图片类型规格 -->
<img @click="changeSelectedStatus(item.values, val)" :class="{ selected: val.selected }" v-if="val.picture"
:src="val.picture" :title="val.name">
<!-- 文字类型规格 -->
<span @click="changeSelectedStatus(item.values, val)" :class="{ selected: val.selected }" v-else>
  {{ val.name}}
</span>

点击规格禁用路径字典

在这里插入图片描述
原理:在电商网站中,一个商品的规格信息会对应不同的库存信息,如果当前选中商品规格的库存为0时,就需要禁用当前规格项,生成路径字典就是为了协助和简化这个匹配过程。
在这里插入图片描述

const getGoods = async () => {
.......
  getPathMap(goods.value)
}

在这里,重点是将每组符合条件的valueName组成一个数组

// 创建路径字典
const getPathMap = (goods) => {
  // 筛选出所有存在库存的数据,即inventory 库存字段>0
  const effectiveSkus = goods.skus.filter(item => item.inventory > 0)
  // 根据有效的数据筛选出子集:[1,2]=>[[1],[2],[1,2]]
  effectiveSkus.forEach(sku => {
    const selectedValArr = sku.specs.map(val => val.valueName)
    console.log(selectedValArr);
  })
}

在这里插入图片描述
在这里插入图片描述
然后封装一个获取子集的接口,[1, 2]=> [[], [1], [2], [1, 2]]

export function getSubsets(arr) {
  const result = [[]];
  for (const num of arr) {
    const len = result.length; //1 , 2
    for (let i = 0; i < len; i++) {
      const subset = result[i];
      result.push([...subset, num]); //扩展运算符展开一个空数组时,它将不会展开任何元素
    }
  }
  return result;
}

紧跟上面输出代码后面调用获取子集的接口实现。

const valueArrPowerSet = getSubsets(selectedValArr)
console.log(valueArrPowerSet);

在这里插入图片描述
之后继续添加如下代码,该函数功能是为每一个子集组合一个唯一编码。

    // 根据子集插入数据
    valueArrPowerSet.forEach(arr => {
      const key = arr.join('-') //进行分割
      if (pathMap[key]) {
        pathMap[key].push(sku.id)
      } else {
        pathMap[key] = [sku.id]
      }
      console.log(pathMap);
    })

在这里插入图片描述
完整代码

const getPathMap = (goods) => {
  const pathMap = {}
  // 筛选出所有存在库存的数据
  const effectiveSkus = goods.skus.filter(item => item.inventory > 0)
  // 根据有效的数据筛选出子集:[1,2]=>[[1],[2],[1,2]]
  effectiveSkus.forEach(sku => {
    const selectedValArr = sku.specs.map(val => val.valueName)
    // 筛选子集
    const valueArrPowerSet = getSubsets(selectedValArr) //[1,2]=>[[],[1],[2],[1,2]]
    // 根据子集插入数据
    valueArrPowerSet.forEach(arr => {
      const key = arr.join('-') //进行分割
      if (pathMap[key]) {
        pathMap[key].push(sku.id)
      } else {
        pathMap[key] = [sku.id]
      }

    })
  })
  return pathMap
}

初始化禁用规格

这次需要根据spec中提供values字段的name完成禁用
在这里插入图片描述
封装一个initDisabledState函数,传入需要匹配的参数

const getGoods = async () => {
....
  const pathMap = getPathMap(goods.value)
  // 初始化规格禁用
  initDisabledState(goods.value.specs, pathMap)
}

主体代码如下,根据每一个val.name的值去匹配pathMap中是否存在该值动态赋予每一个元素disable的值

// 初始化规格函数
const initDisabledState = (specs, pathMap) => {
  specs.forEach(item => {
    item.values.forEach(val => {
      console.log(val.name);
    })
  })
}

在这里插入图片描述

const initDisabledState = (specs, pathMap) => {
  specs.forEach(item => {
    item.values.forEach(val => {
      if (pathMap[val.name]) {
        val.disabled = false //存在就不禁用
      } else {
        val.disabled = true //不存在就禁用
      }
      //可以简化 val.disabled = !pathMap[val.name]
    })
  })
}

最后给元素动态绑定class。但是这样子会出现一个bug,即被禁用的商品缺还可以被点击,这样子是不对的,所以需要在激活样式中进行判断。

<img @click="changeSelectedStatus(item.values, val)" :class="{ selected: val.selected, disabled: val.disabled }">

在这里插入图片描述

const changeSelectedStatus = (row, item) => {
  if (item.disabled) return //被禁用直接不执行后续激活逻辑
  .........
}

组合的时候如何处理禁用状态

在这里插入图片描述
大致思路如下:例如每次点击蓝色+20cm+中国的时候,会生成一个key,拿着该key值去pathMap中匹配,匹配不成功,就禁用当前项
在这里插入图片描述
在这里插入图片描述
封装如下代码

// 获取每次点击组成的规格数组
const getSelectedValues = (specs) => {
  let arr = []
  specs.forEach(spec => {
    // 每行规格当前激活选项有且只有一个或者全部未激活
    const selectedVal = spec.values.find(item => item.selected)
    arr.push(selectedVal ? selectedVal.name : undefined) //处理未点击的其他规格选项
  })
  console.log(arr);
  return arr
}

在这里插入图片描述
效果图如下
在这里插入图片描述

// 点击切换更新禁用状态
const updateDisabledStatus = (specs, pathMap) => {
  specs.forEach((spce, index) => {
    let selectedVal = getSelectedValues(specs) //接收返回的规格数组
    // 填充字段
    spce.values.forEach(val => {
      selectedVal[index] = val.name
      // 过滤掉undefined字段
      let key = selectedVal.filter(item => item).join('-')
      // 进行pathMap匹配
      if (pathMap[key]) {
        val.disabled = false
      } else {
        val.disabled = true
      }
    })
  })
}

最终实现效果如下
在这里插入图片描述

产出有效sku信息

当一个商品所有的规格信息都被选中的时候,才会产出一个规格对象信息,否则就产出一个空对象。用户每次点击的时候都需要进行判断。
getSelectedValues()函数中负责返回结构如下的数据:["数据",undefined,undefined],这个时候只需要判断每个元素是否为undefined即可判断是否全部选中规格。
当规格全部选中的时候,就可以去路径字典中匹配路径所对应的skuId,并将该skuId与返回的数据skus对象中的每一个数据的id进行比对。
只要选择所有的商品规格,那么只会唯一产出一个id信息存放在pathMap中保存。
在这里插入图片描述

let res = {} //接收最终返回的数据
// 激活项
const changeSelectedStatus = (row, item) => {
  .......
  let index = getSelectedValues(goods.value.specs).findIndex(item => item === undefined)
  if (index != -1) {
    console.log(res);
  } else {
    // 组成路径去匹配
    let key = getSelectedValues(goods.value.specs).join("-")
    let skuIds = pathMap[key] //找找唯一id
    res = goods.value.skus.filter(item => { //匹配唯一id所对应的规格信息
      if (item.id === skuIds[0]) {
        return item
      }
    })[0]
    console.log(res);
  }
}

对应输出如下
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值