Vue电商项目实战-4

Vue电商项目实战-4

登录页

  • 路由配置与静态模板导入
    • 导入静态模板
    • 配置路由
    • 在LayoutNav中实现点击按钮进入登陆页面
<script setup>

</script>


<template>
  <div>
    <header class="login-header">
      <div class="container m-top-20">
        <h1 class="logo">
          <RouterLink to="/">小兔鲜</RouterLink>
        </h1>
        <RouterLink class="entry" to="/">
          进入网站首页
          <i class="iconfont icon-angle-right"></i>
          <i class="iconfont icon-angle-right"></i>
        </RouterLink>
      </div>
    </header>
    <section class="login-section">
      <div class="wrapper">
        <nav>
          <a href="javascript:;">账户登录</a>
        </nav>
        <div class="account-box">
          <div class="form">
            <el-form label-position="right" label-width="60px"
              status-icon>
              <el-form-item  label="账户">
                <el-input/>
              </el-form-item>
              <el-form-item label="密码">
                <el-input/>
              </el-form-item>
              <el-form-item label-width="22px">
                <el-checkbox  size="large">
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
              <el-button size="large" class="subBtn">点击登录</el-button>
            </el-form>
          </div>
        </div>
      </div>
    </section>

    <footer class="login-footer">
      <div class="container">
        <p>
          <a href="javascript:;">关于我们</a>
          <a href="javascript:;">帮助中心</a>
          <a href="javascript:;">售后服务</a>
          <a href="javascript:;">配送与验收</a>
          <a href="javascript:;">商务合作</a>
          <a href="javascript:;">搜索推荐</a>
          <a href="javascript:;">友情链接</a>
        </p>
        <p>CopyRight &copy; 小兔鲜儿</p>
      </div>
    </footer>
  </div>
</template>

<style scoped lang='scss'>
.login-header {
  background: #fff;
  border-bottom: 1px solid #e4e4e4;

  .container {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
  }

  .logo {
    width: 200px;

    a {
      display: block;
      height: 132px;
      width: 100%;
      text-indent: -9999px;
      background: url("@/assets/images/logo.png") no-repeat center 18px / contain;
    }
  }

  .sub {
    flex: 1;
    font-size: 24px;
    font-weight: normal;
    margin-bottom: 38px;
    margin-left: 20px;
    color: #666;
  }

  .entry {
    width: 120px;
    margin-bottom: 38px;
    font-size: 16px;

    i {
      font-size: 14px;
      color: $xtxColor;
      letter-spacing: -5px;
    }
  }
}

.login-section {
  background: url('@/assets/images/login-bg.png') no-repeat center / cover;
  height: 488px;
  position: relative;

  .wrapper {
    width: 380px;
    background: #fff;
    position: absolute;
    left: 50%;
    top: 54px;
    transform: translate3d(100px, 0, 0);
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);

    nav {
      font-size: 14px;
      height: 55px;
      margin-bottom: 20px;
      border-bottom: 1px solid #f5f5f5;
      display: flex;
      padding: 0 40px;
      text-align: right;
      align-items: center;

      a {
        flex: 1;
        line-height: 1;
        display: inline-block;
        font-size: 18px;
        position: relative;
        text-align: center;
      }
    }
  }
}

.login-footer {
  padding: 30px 0 50px;
  background: #fff;

  p {
    text-align: center;
    color: #999;
    padding-top: 20px;

    a {
      line-height: 1;
      padding: 0 10px;
      color: #999;
      display: inline-block;

      ~a {
        border-left: 1px solid #ccc;
      }
    }
  }
}

.account-box {
  .toggle {
    padding: 15px 40px;
    text-align: right;

    a {
      color: $xtxColor;

      i {
        font-size: 14px;
      }
    }
  }

  .form {
    padding: 0 20px 20px 20px;

    &-item {
      margin-bottom: 28px;

      .input {
        position: relative;
        height: 36px;

        >i {
          width: 34px;
          height: 34px;
          background: #cfcdcd;
          color: #fff;
          position: absolute;
          left: 1px;
          top: 1px;
          text-align: center;
          line-height: 34px;
          font-size: 18px;
        }

        input {
          padding-left: 44px;
          border: 1px solid #cfcdcd;
          height: 36px;
          line-height: 36px;
          width: 100%;

          &.error {
            border-color: $priceColor;
          }

          &.active,
          &:focus {
            border-color: $xtxColor;
          }
        }

        .code {
          position: absolute;
          right: 1px;
          top: 1px;
          text-align: center;
          line-height: 34px;
          font-size: 14px;
          background: #f5f5f5;
          color: #666;
          width: 90px;
          height: 34px;
          cursor: pointer;
        }
      }

      >.error {
        position: absolute;
        font-size: 12px;
        line-height: 28px;
        color: $priceColor;

        i {
          font-size: 14px;
          margin-right: 2px;
        }
      }
    }

    .agree {
      a {
        color: #069;
      }
    }

    .btn {
      display: block;
      width: 100%;
      height: 40px;
      color: #fff;
      text-align: center;
      line-height: 40px;
      background: $xtxColor;

      &.disabled {
        background: #cfcdcd;
      }
    }
  }

  .action {
    padding: 20px 40px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .url {
      a {
        color: #999;
        margin-left: 10px;
      }
    }
  }
}

.subBtn {
  background: $xtxColor;
  width: 100%;
  color: #fff;
}
</style>
 {
      path:'/login',
      component:login
    },
<ul>
        <template v-if="false">
          <li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li>
          <li>
            <el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
              <template #reference>
                <a href="javascript:;">退出登录</a>
              </template>
            </el-popconfirm>
          </li>
          <li><a href="javascript:;">我的订单</a></li>
          <li><a href="javascript:;">会员中心</a></li>
        </template>
        <template v-else>
          <li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li>
          <li><a href="javascript:;">帮助中心</a></li>
          <li><a href="javascript:;">关于我们</a></li>
        </template>
      </ul>
  • 表单检验功能
    • ElementPlus内置了表单检验功能,只需要按照组件要求配置必要参数即可:
      • el-form:绑定表单对象和规则对象
      • el-form-item:绑定使用的规则手段
      • el-input:双向绑定表单数据
    • 表单校验步骤:
      • 按照接口字段准备表单对象并绑定
      • 按照产品要求准备规则对象并绑定
      • 指定表单域的校验字段名
      • 把表单对象进行双向绑定
import { ref } from 'vue';

//准备表单对象
const form = ref({
    account:'',
    password:''
})

//准备规则对象
const rules = {
    account:[
        { required:true,message:'用户名不能为空',trigger:'blur' }
    ],
    password:[
        { required:true,message:'密码不能为空',trigger:'blur' },
        { min:6,max:14,message:'密码长度为6-14个字符',trigger:'blur' }
    ]
}
<el-form :model="form" :rules="rules" label-position="right" label-width="60px"
              status-icon>
              <el-form-item prop="account" label="账户">
                <el-input v-model="form.account"/>
              </el-form-item>
              <el-form-item prop="password" label="密码">
                <el-input v-model="form.password"/>
              </el-form-item>
              <el-form-item label-width="22px">
                <el-checkbox  size="large">
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
              <el-button size="large" class="subBtn">点击登录</el-button>
            </el-form>
  • 自定义表单校验
    • 若想要定制一些特殊的校验规则,可以使用自定义规则
    • 校验逻辑:勾选了协议框,通过校验,否则不通过校验
 import { ref } from 'vue';

//准备表单对象
const form = ref({
    account:'',
    password:'',
    agree:true
})

//准备规则对象
const rules = {
    account:[
        { required:true,message:'用户名不能为空',trigger:'blur' }
    ],
    password:[
        { required:true,message:'密码不能为空',trigger:'blur' },
        { min:6,max:14,message:'密码长度为6-14个字符',trigger:'blur' }
    ],
    agree:[
        {
            validator:(rule,value,callback)=>{
                console.log(value)
                if(value){
                    callback()
                }else{
                    callback(new Error('请勾选协议!'))
                }
            }
        }
    ]
}

<el-form-item prop="agree" label-width="22px">
                <el-checkbox v-model="form.agree" size="large" >
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
  • 统一校验
    • 在点击登录时需要对所有表单进行统一校验
    • 获取form组件实例
    • 调用实例方法

//获取form实例做统一校验
const formRef = ref(null)
const doLogin =()=>{
    formRef.value.validate((valid)=>{
        console.log(valid)
        if(valid){
            //Login
        }
    })
}
 <el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="60px"
              status-icon>
              <el-form-item prop="account" label="账户">
                <el-input v-model="form.account"/>
              </el-form-item>
              <el-form-item prop="password" label="密码">
                <el-input v-model="form.password"/>
              </el-form-item>
              <el-form-item prop="agree" label-width="22px">
                <el-checkbox v-model="form.agree" size="large" >
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
              <el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button>
            </el-form>
  • 基础功能实现
    • 封装登录接口
    • 调用登录接口
      • 登录成功后逻辑处理:提示用户,跳转首页
      • 登录失败的逻辑业务:抛出错误提醒
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
import { useRouter } from 'vue-router';


//获取form实例做统一校验
const formRef = ref(null)
const router = useRouter()
const doLogin =()=>{
    const { account,password } = form.value
    formRef.value.validate(async(valid)=>{
        console.log(valid)
        if(valid){
            //Login
            const res =await loginAPI({ account,password })
            console.log(res)
            ElMessage({ type:'success',message:'登录成功'})
            router.replace({path:'/'})
        }
    })
}
import request from '@/utils/https'

export const loginAPI =({ account,password}) => {
    return request({
        url:'/login',
        method:'POST',
        data:{
            account,
            password
        }
    })
}

  // axios响应式拦截器
  httpInstance.interceptors.response.use(res => res.data, e => {
    ElMessage({
      type:'warning',
      message:e.response.data.message
    })
    return Promise.reject(e)
  })
  • Pinia管理用户数据
    • 和数据相关的所有操作(action+state)都放在Pinia里面
    • 组件触发action
    • action函数内部接口获取数据
    • 用户信息存入
import { defineStore } from "pinia";
import { ref } from 'vue';
import { loginAPI } from "@/apis/user";

export const useUserStore = defineStore('user',()=>{
    const userInfo = ref({})
    const getUserInfo = async({ account,password })=>{
        const res = await loginAPI ({ account,password })
        userInfo.value = res.result
    }
    return{
        userInfo,
        getUserInfo
    }
})
  await useStore.getUserInfo({ account,password })
  • Pinia数据用户持久化
    • 持久化用户数据说明:
      • 用户数据中有一个关键数据Token,用于标识用户是否登录,过一段时间才会过期
      • Pinia的存储是基于内存的,刷新就会丢失,为保证登录后不消失,需要配合持久化存储
      • 目的:保持token不丢失,保持登录状态
      • 操作state时会自动把用户数据在本地localStorage也保存一份,刷新时从localStorage先取
      • npm i pinia-plugin-persistedstate
      • 在main.js中引入pinia-plugin-persistedstate,并注册持久化插件
      • 在user.js中创建store时,把persistent设置为true
import '@/styles/common.scss'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

import App from './App.vue'
import router from './router'

import '@/styles/common.scss'

//引入懒加载指令插件并注册
import { lazyPlugin } from '@/directives'
//引入全局组件插件
import { componentPlugin } from '@/components'

const app = createApp(App)
const pinia = createPinia()

pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(lazyPlugin)
app.use(componentPlugin)
app.mount('#app')


import { defineStore } from "pinia";
import { ref } from 'vue';
import { loginAPI } from "@/apis/user";

export const useUserStore = defineStore('user',()=>{
    const userInfo = ref({})
    const getUserInfo = async({ account,password })=>{
        const res = await loginAPI ({ account,password })
        userInfo.value = res.result
    }
    return{
        userInfo,
        getUserInfo
    }
},{
    persist: true,
})

在这里插入图片描述

  • 登录和非登录状态下的状态切换
    • 多模板适配的通用思路:有几个需要适配的模板就写几个templa段,用条件渲染v-if控制显示即可
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore() 
</script>

<template>
  <nav class="app-topnav">
    <div class="container">
      <ul>
        <template v-if="userStore.userInfo.token">
          <li><a href="javascript:;"><i class="iconfont icon-user"></i>{{ userStore.userInfo.account }}</a></li>
          <li>
            <el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
              <template #reference>
                <a href="javascript:;">退出登录</a>
              </template>
            </el-popconfirm>
          </li>
          <li><a href="javascript:;">我的订单</a></li>
          <li><a href="javascript:;">会员中心</a></li>
        </template>
        <template v-else>
          <li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li>
          <li><a href="javascript:;">帮助中心</a></li>
          <li><a href="javascript:;">关于我们</a></li>
        </template>
      </ul>
    </div>
  </nav>
</template>


<style scoped lang="scss">
.app-topnav {
  background: #333;
  ul {
    display: flex;
    height: 53px;
    justify-content: flex-end;
    align-items: center;
    li {
      a {
        padding: 0 15px;
        color: #cdcdcd;
        line-height: 1;
        display: inline-block;

        i {
          font-size: 14px;
          margin-right: 2px;
        }

        &:hover {
          color: $xtxColor;
        }
      }

      ~li {
        a {
          border-left: 2px solid #666;
        }
      }
    }
  }
}
</style>

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

  • 请求拦截器携带token
    • Token作为用户标识,很多个接口需要通过token才能获取数据,所有需要在接口调用时携带token,为了统一控制采用响应拦截器的方案
      • 从pinia获取token数据
      • 按照后端的要求拼接token数据
import { useUserStore } from "@/stores/user";
// axios请求拦截器
httpInstance.interceptors.request.use(config => {
 const userStore = useUserStore()
 const token = userStore.userInfo.token
 if(token){
   config.headers.Authorization = `Bearer ${token}`
 }
   return config
 }, e => Promise.reject(e))

在这里插入图片描述

  • 退出登录业务实现
    • 点击退出登录弹确认框
    • 点击确认按钮实现退出登录逻辑
      • 清理当前用户信息:在user.js中写入clearUserInfo实现清除信息,在LayoutNav中使用
      • 跳转至登录页面:使用useRouter实现
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router';

const userStore = useUserStore() 
const router = useRouter()
const confirm = () => {
  console.log('用户要退出登录')
  userStore.clearUserInfo()
  router.push('/login')
}
 <li>
            <el-popconfirm @confirm="confirm" title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
              <template #reference>
                <a href="javascript:;">退出登录</a>
              </template>
            </el-popconfirm>
          </li>

在这里插入图片描述

  • Token失效401拦截处理
    • 业务背景:用户如果一段时间不做任何操作,Token就会失效,失效的token再去请求接口就会报401错误
    • 在axios响应拦截器中做统一处理
      • 失败回调拦截401
        • 清除掉过期的用户信息
        • 跳转至登录页

购物车页

  • 本地加入购物车实现
    • 封装cartStore
    • 组件点击添加按钮
      • 选择了规格:调用action添加
        • 添加过:count+1
        • 未添加:push出来
      • 未选择规格:提示用户选择规格
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore = defineStore('cart', () => {
    //1.定义state - cartList
    const cartList = ref([])
    //2.定义action - addCart
    const addCart = async (goods) => {
        // 添加购物车操作
        // 已添加过 - count + 1
        // 没有添加过 - 直接push
        // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
        const item = cartList.value.find((item) => goods.skuId === item.skuId)
        if (item) {
            // 找到了
            item.count++
        } else {
            // 没找到
            cartList.value.push(goods)
        }
    }
    return {
        cartList,
        addCart
    }

}, {
    persist: true
})
import { useCartStore } from '@/stores/cartStore.js'
const cartStore = useCartStore()

let skuObj = {}
const Skuchange = (sku) => {
    console.log(sku)
    skuObj =sku
}

const count = ref(1)
const countChange = (count)=>{
    console.log(count)
}

const addCart = () => {
    if (skuObj.skuId) {
        //规格已经选择 触发action
        cartStore.addCart({
            id: goods.value.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.warning('请选择规格')
    }
}
  <!-- 数据组件 -->
              <el-input-number v-model="count"  @change="countChange" />
              <!-- 按钮组件 -->
              <div>
                <el-button size="large" class="btn" @click="addCart">
                  加入购物车
                </el-button>
              </div>

在这里插入图片描述

  • 头部购物车列表渲染
    • 准备头部购物车组件
    • 从Pinia中获取数据渲染列表,获取列表长度并渲染
<script setup>
import { useCartStore } from '@/stores/cartStore'
const cartStore = useCartStore()

</script>

<template>
  <div class="cart">
    <a class="curr" href="javascript:;">
      <i class="iconfont icon-cart"></i><em>{{ cartStore.cartList.length }}</em>
    </a>
    <div class="layer">
      <div class="list">
        <div class="item" v-for="i in cartStore.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" type="primary" >去购物车结算</el-button>
      </div>
    </div>
</div>
</template>

<style scoped lang="scss">
.cart {
  width: 50px;
  position: relative;
  z-index: 600;

  .curr {
    height: 32px;
    line-height: 32px;
    text-align: center;
    position: relative;
    display: block;

    .icon-cart {
      font-size: 22px;
    }

    em {
      font-style: normal;
      position: absolute;
      right: 0;
      top: 0;
      padding: 1px 6px;
      line-height: 1;
      background: $helpColor;
      color: #fff;
      font-size: 12px;
      border-radius: 10px;
      font-family: Arial;
    }
  }

  &:hover {
    .layer {
      opacity: 1;
      transform: none;
    }
  }

  .layer {
    opacity: 0;
    transition: all 0.4s 0.2s;
    transform: translateY(-200px) scale(1, 0);
    width: 400px;
    height: 400px;
    position: absolute;
    top: 50px;
    right: 0;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    background: #fff;
    border-radius: 4px;
    padding-top: 10px;

    &::before {
      content: "";
      position: absolute;
      right: 14px;
      top: -10px;
      width: 20px;
      height: 20px;
      background: #fff;
      transform: scale(0.6, 1) rotate(45deg);
      box-shadow: -3px -3px 5px rgba(0, 0, 0, 0.1);
    }

    .foot {
      position: absolute;
      left: 0;
      bottom: 0;
      height: 70px;
      width: 100%;
      padding: 10px;
      display: flex;
      justify-content: space-between;
      background: #f8f8f8;
      align-items: center;

      .total {
        padding-left: 10px;
        color: #999;

        p {
          &:last-child {
            font-size: 18px;
            color: $priceColor;
          }
        }
      }
    }
  }

  .list {
    height: 310px;
    overflow: auto;
    padding: 0 10px;

    &::-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;
    }

    .item {
      border-bottom: 1px solid #f5f5f5;
      padding: 10px 0;
      position: relative;

      i {
        position: absolute;
        bottom: 38px;
        right: 0;
        opacity: 0;
        color: #666;
        transition: all 0.5s;
      }

      &:hover {
        i {
          opacity: 1;
          cursor: pointer;
        }
      }

      a {
        display: flex;
        align-items: center;

        img {
          height: 80px;
          width: 80px;
        }

        .center {
          padding: 0 10px;
          width: 200px;

          .name {
            font-size: 16px;
          }

          .attr {
            color: #999;
            padding-top: 5px;
          }
        }

        .right {
          width: 100px;
          padding-right: 20px;
          text-align: center;

          .price {
            font-size: 16px;
            color: $priceColor;
          }

          .count {
            color: #999;
            margin-top: 5px;
            font-size: 16px;
          }
        }
      }
    }
  }
}
</style>
 <!-- 头部购物车 -->
      <HeaderCart />
  • 头部购物车删除功能实现
    • 编写删除的action函数,实现删除逻辑:
      • 找到要删除项的下标值—splice
      • 使用数组的过滤方法—filter
    • 组件中调用action并传递skuId
  const delCart = (skuId) =>{
        const idx = cartList.value.findIndex((item)=>skuId === item.skuId)
        cartList.value.splice(idx,1)
    } 

    return {
        cartList,
        addCart,
        delCart
    }
  <i class="iconfont icon-close-new" @click="cartStore.delCart(i.skuId)"></i>
  • 头部购物车统计计算
    • 用计算属性实现,在cartStore写相应的action函数
    • 商品总数计算逻辑:商品列表中所有的count相加
    • 商品价格计算逻辑:商品列表中所有的count*price相加
 //计算属性
    const allCount = computed(()=> cartList.value.reduce((a,c)=>a+c.count,0))
    const allPrice = computed(()=> cartList.value.reduce((a,c)=>a+c.count*c.price,0))

    return {
        cartList,
        addCart,
        delCart,
        allCount,
        allPrice
    }
<div class="foot">
        <div class="total">
          <p>{{ cartStore.allCount }} 件商品</p>
          <p>&yen; {{ cartStore.allPrice }} </p>
        </div>
        <el-button size="large" type="primary" >去购物车结算</el-button>
      </div>
    </div>

在这里插入图片描述

  • 列表购物车基础数据渲染
    • 准备静态模板
    • 配置路由:cartlist
    • 用click实现路由跳转
    • 运用useCartStore实现列表渲染
<script setup>
import { useCartStore } from '@/stores/cartStore'
const cartStore = useCartStore()

</script>

<template>
 <div class="xtx-cart-page">
   <div class="container m-top-20">
     <div class="cart">
       <table>
         <thead>
           <tr>
             <th width="120">
               <el-checkbox/>
             </th>
             <th width="400">商品信息</th>
             <th width="220">单价</th>
             <th width="180">数量</th>
             <th width="180">小计</th>
             <th width="140">操作</th>
           </tr>
         </thead>
         <!-- 商品列表 -->
         <tbody>
           <tr v-for="i in cartStore.cartList" :key="i.id">
             <td>
               <el-checkbox />
             </td>
             <td>
               <div class="goods">
                 <RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink>
                 <div>
                   <p class="name ellipsis">
                     {{ i.name }}
                   </p>
                 </div>
               </div>
             </td>
             <td class="tc">
               <p>&yen;{{ i.price }}</p>
             </td>
             <td class="tc">
               <el-input-number v-model="i.count" />
             </td>
             <td class="tc">
               <p class="f16 red">&yen;{{ (i.price * i.count).toFixed(2) }}</p>
             </td>
             <td class="tc">
               <p>
                 <el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)">
                   <template #reference>
                     <a href="javascript:;">删除</a>
                   </template>
                 </el-popconfirm>
               </p>
             </td>
           </tr>
           <tr v-if="cartStore.cartList.length === 0">
             <td colspan="6">
               <div class="cart-none">
                 <el-empty description="购物车列表为空">
                   <el-button type="primary">随便逛逛</el-button>
                 </el-empty>
               </div>
             </td>
           </tr>
         </tbody>

       </table>
     </div>
     <!-- 操作栏 -->
     <div class="action">
       <div class="batch">10 件商品,已选择 2 件,商品合计:
         <span class="red">¥ 200.00 </span>
       </div>
       <div class="total">
         <el-button size="large" type="primary" >下单结算</el-button>
       </div>
     </div>
   </div>
 </div>
</template>

<style scoped lang="scss">
.xtx-cart-page {
 margin-top: 20px;

 .cart {
   background: #fff;
   color: #666;

   table {
     border-spacing: 0;
     border-collapse: collapse;
     line-height: 24px;

     th,
     td {
       padding: 10px;
       border-bottom: 1px solid #f5f5f5;

       &:first-child {
         text-align: left;
         padding-left: 30px;
         color: #999;
       }
     }

     th {
       font-size: 16px;
       font-weight: normal;
       line-height: 50px;
     }
   }
 }

 .cart-none {
   text-align: center;
   padding: 120px 0;
   background: #fff;

   p {
     color: #999;
     padding: 20px 0;
   }
 }

 .tc {
   text-align: center;

   a {
     color: $xtxColor;
   }

   .xtx-numbox {
     margin: 0 auto;
     width: 120px;
   }
 }

 .red {
   color: $priceColor;
 }

 .green {
   color: $xtxColor;
 }

 .f16 {
   font-size: 16px;
 }

 .goods {
   display: flex;
   align-items: center;

   img {
     width: 100px;
     height: 100px;
   }

   >div {
     width: 280px;
     font-size: 16px;
     padding-left: 10px;

     .attr {
       font-size: 14px;
       color: #999;
     }
   }
 }

 .action {
   display: flex;
   background: #fff;
   margin-top: 20px;
   height: 80px;
   align-items: center;
   font-size: 16px;
   justify-content: space-between;
   padding: 0 30px;

   .xtx-checkbox {
     color: #999;
   }

   .batch {
     a {
       margin-left: 20px;
     }
   }

   .red {
     font-size: 18px;
     margin-right: 20px;
     font-weight: bold;
   }
 }

 .tit {
   color: #666;
   font-size: 16px;
   font-weight: normal;
   line-height: 50px;
 }

}
</style>
import cartlist from '@/views/CartList/index.vue'

{
          path:'cartlist',
          component:cartlist,
        }
      ]
 <el-button size="large" type="primary" @click="$router.push('/cartlist')">去购物车结算</el-button>
  • 列表购物车单选功能
    • 核心思路:把单选框状态和Pinia中store状态保持同步
    • v-model指令不适合进行命令式的操作,所以把v-model回退为一般模式:model-value和@change的配合
  const singleCheck = (skuId,selected) =>{
        const item = cartList.value.find((item) => item.skuId===skuId)
        item.selected = selected
    }

    return {
        cartList,
        addCart,
        delCart,
        allCount,
        allPrice,
        singleCheck
    }

const singleCheck=(i,selected)=>{
    console.log(i,selected)
    cartStore.singleCheck(i.skuId,selected)

}
 <!--单选框-->
                <el-checkbox :model-value="i.selected" @change="(selected)=> singleCheck(i,selected)" />
  • 购物车列表全选
    • 操作单选决定多选:当cartStore所有选项为true时,isAll才为true
    • 操作多选决定单选:cartList所有项的selected都要跟着变
  const isAll = computed(()=> cartList.value.every((item)=>item.selected))

    //全选功能
    const checkAll = (selected) =>{
         // 把cartList中的每一项的selected都设置为当前的全选框状态
         cartList.value.forEach(item => item.selected = selected)

    }

const checkAll =(selected) =>{
    cartStore.checkAll(selected)
}
<el-checkbox :model-value="cartStore.isAll" @change="checkAll"/>
  • 购物车列表统计数据实现
    • 已选择数量 = cartList中的所有selected字段为true项的count之和
    • 商品合计 = cartList中所有的selected字段为true项的count*price之和

    const selectedCount = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.count,0))
    const selectedPrice = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.count*c.price,0))
<div class="batch">{{ cartStore.allCount }} 件商品,已选择 {{ cartStore.selectedCount }} 件,商品合计:
          <span class="red">¥ {{  cartStore.selectedPrice.toFixed(2)  }} </span>
        </div>
  • 加入购物车
    • 找到加入购物车的action函数
    • 判断是否登录,若是:
      • 调用加入购物车接口
      • 调用获取购物车列表接口
      • 用接口购物车列表覆盖本地购物车列表
import request from '@/utils/https'

export const insertCartAPI =({skuId,count}) =>{
    return request({
        url:'/member/cart',
        method:'POST',
        data:{
            skuId,
            count
        }
    })
}

export const findNewCartListAPI=()=>{
    return request({
        url:'/member/cart'
    })
}
//2.定义action - addCart
    const addCart = async (goods) => {
        const { skuId,count } = goods
        if(isLogin.value){
            await insertCartAPI({ skuId,count })
            const res = await findNewCartListAPI()
            cartList.value = res.result
        }else{
            // 添加购物车操作
        // 已添加过 - count + 1
        // 没有添加过 - 直接push
        // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
        const item = cartList.value.find((item) => goods.skuId === item.skuId)
        if (item) {
            // 找到了
            item.count++
        } else {
            // 没找到
            cartList.value.push(goods)
        }
        }
    }

在这里插入图片描述

  • 删除购物车
    • 找到删除购物车的action函数
    • 判断是否登录,如果是
      • 调用删除购物车的接口
      • 调用获取购物车列表接口
      • 用接口购物车列表覆盖本地购物车列表
const delCart = async(skuId) =>{
        if(isLogin.value){
            await delCartAPI([skuId])
            updateNewList()
        }else{
            const idx = cartList.value.findIndex((item)=>skuId === item.skuId)
            cartList.value.splice(idx,1)
        }
    }

    //获取列表操作
    const updateNewList =async() =>{
        const res = await findNewCartListAPI()
        cartList.value = res.result
    }

export const delCartAPI = (ids) =>{
    return request({
        url:'/member/cart',
        method:'DELETE',
        data:{
            ids
        }
    })
}

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

  • 清空购物车信息
    • 业务需要:在用户退出登录时,除了清除用户信息之外,也需要把购物车数据清除
    • 在购物车store中补充清除购物车的action
    • userStore中找到退出登录action,执行清理业务
    //清除列表操作
      const clearCart = () =>{
          cartList.value=[]
      }
    
import { useCartStore } from "./cartStore"
export const useUserStore = defineStore('user',()=>{
    const cartStore = useCartStore()
    const userInfo = ref({})
    const getUserInfo = async({ account,password })=>{
        const res = await loginAPI ({ account,password })
        userInfo.value = res.result
    }
    const clearUserInfo =()=>{
        userInfo.value = {}
        cartStore.clearCart
    }
    return{
        userInfo,
        getUserInfo,
        clearUserInfo
    }
},{
    persist: true,
})
  • 合并购物车操作
    • 为了在未登录的时候的操作做效
    • 在用户登录时,把本地购物车与服务端购物车数据进行合并
      • 登录时调用合并购物车接口
      • 获取最新的购物车列表
      • 覆盖本地购物车列表

export const mergeCartAPI = (data) => {
    return request({
        url:'/member/cart/merge',
        method:'POST',
        data
    })

}
import { defineStore } from "pinia";
import { ref } from 'vue';
import { loginAPI } from "@/apis/user";
import { useCartStore } from "./cartStore"
import { mergeCartAPI } from "@/apis/cart";
export const useUserStore = defineStore('user',()=>{
    const cartStore = useCartStore()
    const userInfo = ref({})
    const getUserInfo = async({ account,password })=>{
        const res = await loginAPI ({ account,password })
        userInfo.value = res.result
        //合并购物车的操作
        await mergeCartAPI(cartStore.cartList.map(item=>{
            return{
                skuId:item.skuId,
                selected:item.selected,
                count:item.count
            }
        }))
        cartStore.updateNewList()
    }
    const clearUserInfo =()=>{
        userInfo.value = {}
        cartStore.clearCart()
    }
    return{
        userInfo,
        getUserInfo,
        clearUserInfo
    }
},{
    persist: true,
})

结算模块

  • 路由配置和基础数据渲染
    • 路由配置:
      • 准备路由组件
      • 配置路由关系
      • 配置路由跳转
    • 基础组件渲染:
      • 准备接口
      • 获取数据
      • 渲染默认地址和商品列表以及统计数据
import request from '@/utils/https'

export const getCheckInfoAPI =() => {
    return request({
        url:'/member/order/pre'
    })
}
import checkout from '@/views/Checkout/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path:'/',
      component:layout,
      children:[
        {
          path:'',
          component:home,
        },
        {
          path:'category/:id',
          component:category,
        },
        {
          path:'category/sub/:id',
          component:subcategory,
        },
        {
          path:'detail/:id',
          component:detail,
        },
        {
          path:'cartlist',
          component:cartlist,
        },
        {
          path:'checkout',
          component:checkout
        }
      ]
    },
    {
      path:'/login',
      component:login
    },
  ],
  //路由行为定制
  scrollBehavior(){
    return{
      top:0
    }
  }
})

<script setup>
import { getCheckInfoAPI } from '@/apis/checkout.js'
import { onMounted,ref } from 'vue';
const checkInfo = ref({})  // 订单对象
const curAddress = ref({})  // 地址对象
const getCheckInfo = async() => {
    const res = await getCheckInfoAPI()
    checkInfo.value = res.result
    const item = checkInfo.value.userAddresses.find(item => item.isDefault ===0)
    curAddress.value = item
}
onMounted(()=>getCheckInfo())


</script>

<template>
  <div class="xtx-pay-checkout-page">
    <div class="container">
      <div class="wrapper">
        <!-- 收货地址 -->
        <h3 class="box-title">收货地址</h3>
        <div class="box-body">
          <div class="address">
            <div class="text">
              <div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div>
              <ul v-else>
                <li><span><i /><i />人:</span>{{ curAddress.receiver }}</li>
                <li><span>联系方式:</span>{{ curAddress.contact }}</li>
                <li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li>
              </ul>
            </div>
            <div class="action">
              <el-button size="large" @click="toggleFlag = true">切换地址</el-button>
              <el-button size="large" @click="addFlag = true">添加地址</el-button>
            </div>
          </div>
        </div>
        <!-- 商品信息 -->
        <h3 class="box-title">商品信息</h3>
        <div class="box-body">
          <table class="goods">
            <thead>
              <tr>
                <th width="520">商品信息</th>
                <th width="170">单价</th>
                <th width="170">数量</th>
                <th width="170">小计</th>
                <th width="170">实付</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="i in checkInfo.goods" :key="i.id">
                <td>
                  <a href="javascript:;" class="info">
                    <img :src="i.picture" alt="">
                    <div class="right">
                      <p>{{ i.name }}</p>
                      <p>{{ i.attrsText }}</p>
                    </div>
                  </a>
                </td>
                <td>&yen;{{ i.price }}</td>
                <td>{{ i.price }}</td>
                <td>&yen;{{ i.totalPrice }}</td>
                <td>&yen;{{ i.totalPayPrice }}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <!-- 配送时间 -->
        <h3 class="box-title">配送时间</h3>
        <div class="box-body">
          <a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>
          <a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>
          <a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>
        </div>
        <!-- 支付方式 -->
        <h3 class="box-title">支付方式</h3>
        <div class="box-body">
          <a class="my-btn active" href="javascript:;">在线支付</a>
          <a class="my-btn" href="javascript:;">货到付款</a>
          <span style="color:#999">货到付款需付5元手续费</span>
        </div>
        <!-- 金额明细 -->
        <h3 class="box-title">金额明细</h3>
        <div class="box-body">
          <div class="total">
            <dl>
              <dt>商品件数:</dt>
              <dd>{{ checkInfo.summary?.goodsCount }}</dd>
            </dl>
            <dl>
              <dt>商品总价:</dt>
              <dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd>
            </dl>
            <dl>
              <dt><i></i>费:</dt>
              <dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd>
            </dl>
            <dl>
              <dt>应付总额:</dt>
              <dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd>
            </dl>
          </div>
        </div>
        <!-- 提交订单 -->
        <div class="submit">
          <el-button type="primary" size="large" >提交订单</el-button>
        </div>
      </div>
    </div>
  </div>
  <!-- 切换地址 -->
  <!-- 添加地址 -->
</template>

<style scoped lang="scss">
.xtx-pay-checkout-page {
  margin-top: 20px;

  .wrapper {
    background: #fff;
    padding: 0 20px;

    .box-title {
      font-size: 16px;
      font-weight: normal;
      padding-left: 10px;
      line-height: 70px;
      border-bottom: 1px solid #f5f5f5;
    }

    .box-body {
      padding: 20px 0;
    }
  }
}

.address {
  border: 1px solid #f5f5f5;
  display: flex;
  align-items: center;

  .text {
    flex: 1;
    min-height: 90px;
    display: flex;
    align-items: center;

    .none {
      line-height: 90px;
      color: #999;
      text-align: center;
      width: 100%;
    }

    >ul {
      flex: 1;
      padding: 20px;

      li {
        line-height: 30px;

        span {
          color: #999;
          margin-right: 5px;

          >i {
            width: 0.5em;
            display: inline-block;
          }
        }
      }
    }

    >a {
      color: $xtxColor;
      width: 160px;
      text-align: center;
      height: 90px;
      line-height: 90px;
      border-right: 1px solid #f5f5f5;
    }
  }

  .action {
    width: 420px;
    text-align: center;

    .btn {
      width: 140px;
      height: 46px;
      line-height: 44px;
      font-size: 14px;

      &:first-child {
        margin-right: 10px;
      }
    }
  }
}

.goods {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;

  .info {
    display: flex;
    text-align: left;

    img {
      width: 70px;
      height: 70px;
      margin-right: 20px;
    }

    .right {
      line-height: 24px;

      p {
        &:last-child {
          color: #999;
        }
      }
    }
  }

  tr {
    th {
      background: #f5f5f5;
      font-weight: normal;
    }

    td,
    th {
      text-align: center;
      padding: 20px;
      border-bottom: 1px solid #f5f5f5;

      &:first-child {
        border-left: 1px solid #f5f5f5;
      }

      &:last-child {
        border-right: 1px solid #f5f5f5;
      }
    }
  }
}

.my-btn {
  width: 228px;
  height: 50px;
  border: 1px solid #e4e4e4;
  text-align: center;
  line-height: 48px;
  margin-right: 25px;
  color: #666666;
  display: inline-block;

  &.active,
  &:hover {
    border-color: $xtxColor;
  }
}

.total {
  dl {
    display: flex;
    justify-content: flex-end;
    line-height: 50px;

    dt {
      i {
        display: inline-block;
        width: 2em;
      }
    }

    dd {
      width: 240px;
      text-align: right;
      padding-right: 70px;

      &.price {
        font-size: 20px;
        color: $priceColor;
      }
    }
  }
}

.submit {
  text-align: right;
  padding: 60px;
  border-top: 1px solid #f5f5f5;
}

.addressWrapper {
  max-height: 500px;
  overflow-y: auto;
}

.text {
  flex: 1;
  min-height: 90px;
  display: flex;
  align-items: center;

  &.item {
    border: 1px solid #f5f5f5;
    margin-bottom: 10px;
    cursor: pointer;

    &.active,
    &:hover {
      border-color: $xtxColor;
      background: lighten($xtxColor, 50%);
    }

    >ul {
      padding: 10px;
      font-size: 14px;
      line-height: 30px;
    }
  }
}
</style>
  <div class="total">
          <el-button size="large" type="primary" @click="$router.push('/checkout')">下单结算</el-button>
        </div>
  • 地址切换
    • 打开弹窗交互实现
      • 准备弹框组件
      • 组件v-model绑定响应式数据
      • 点击按钮控制响应式数据
      • 渲染可选地址列表
    • 地址激活交互实现
      • 点击时记录一个当前激活地址对象activeAdress,点击哪个地址就把哪个地址记录下来
      • 通过动态类名:class控制激活样式类型active是否存在,判断条件为:激活地址对象id===当前项id
<script setup>
import { getCheckInfoAPI } from '@/apis/checkout.js'
import { onMounted,ref } from 'vue';
const checkInfo = ref({})  // 订单对象
const curAddress = ref({})  // 地址对象
const getCheckInfo = async() => {
   const res = await getCheckInfoAPI()
   checkInfo.value = res.result
   const item = checkInfo.value.userAddresses.find(item => item.isDefault ===0)
   curAddress.value = item
}
onMounted(()=>getCheckInfo())

const showDialog = ref(false)

const activeAdress = ref({})
const switchAdress = (item) =>{
   activeAdress.value=item
}
const confirm = () =>{
   curAddress.value = activeAdress.value
   showDialog.value = false
   activeAdress.value= {}
}
</script>

<template>
 <div class="xtx-pay-checkout-page">
   <div class="container">
     <div class="wrapper">
       <!-- 收货地址 -->
       <h3 class="box-title">收货地址</h3>
       <div class="box-body">
         <div class="address">
           <div class="text">
             <div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div>
             <ul v-else>
               <li><span><i /><i />人:</span>{{ curAddress.receiver }}</li>
               <li><span>联系方式:</span>{{ curAddress.contact }}</li>
               <li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li>
             </ul>
           </div>
           <div class="action">
             <el-button size="large" @click="showDialog = true">切换地址</el-button>
             <el-button size="large" @click="addAdress = true">添加地址</el-button>
           </div>
         </div>
       </div>
       <!-- 商品信息 -->
       <h3 class="box-title">商品信息</h3>
       <div class="box-body">
         <table class="goods">
           <thead>
             <tr>
               <th width="520">商品信息</th>
               <th width="170">单价</th>
               <th width="170">数量</th>
               <th width="170">小计</th>
               <th width="170">实付</th>
             </tr>
           </thead>
           <tbody>
             <tr v-for="i in checkInfo.goods" :key="i.id">
               <td>
                 <a href="javascript:;" class="info">
                   <img :src="i.picture" alt="">
                   <div class="right">
                     <p>{{ i.name }}</p>
                     <p>{{ i.attrsText }}</p>
                   </div>
                 </a>
               </td>
               <td>&yen;{{ i.price }}</td>
               <td>{{ i.price }}</td>
               <td>&yen;{{ i.totalPrice }}</td>
               <td>&yen;{{ i.totalPayPrice }}</td>
             </tr>
           </tbody>
         </table>
       </div>
       <!-- 配送时间 -->
       <h3 class="box-title">配送时间</h3>
       <div class="box-body">
         <a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>
         <a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>
         <a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>
       </div>
       <!-- 支付方式 -->
       <h3 class="box-title">支付方式</h3>
       <div class="box-body">
         <a class="my-btn active" href="javascript:;">在线支付</a>
         <a class="my-btn" href="javascript:;">货到付款</a>
         <span style="color:#999">货到付款需付5元手续费</span>
       </div>
       <!-- 金额明细 -->
       <h3 class="box-title">金额明细</h3>
       <div class="box-body">
         <div class="total">
           <dl>
             <dt>商品件数:</dt>
             <dd>{{ checkInfo.summary?.goodsCount }}</dd>
           </dl>
           <dl>
             <dt>商品总价:</dt>
             <dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd>
           </dl>
           <dl>
             <dt><i></i>费:</dt>
             <dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd>
           </dl>
           <dl>
             <dt>应付总额:</dt>
             <dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd>
           </dl>
         </div>
       </div>
       <!-- 提交订单 -->
       <div class="submit">
         <el-button type="primary" size="large" >提交订单</el-button>
       </div>
     </div>
   </div>
 </div>
 <!-- 切换地址 -->
 <el-dialog v-model="showDialog" title="切换收货地址" width="30%" center>
 <div class="addressWrapper">
   <div class="text item" :class="{ active:activeAdress.id === item.id }" @click="switchAdress(item)" v-for="item in checkInfo.userAddresses"  :key="item.id">
     <ul>
     <li><span><i /><i />人:</span>{{ item.receiver }} </li>
     <li><span>联系方式:</span>{{ item.contact }}</li>
     <li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li>
     </ul>
   </div>
 </div>
 <template #footer>
   <span class="dialog-footer">
     <el-button>取消</el-button>
     <el-button type="primary" @click="confirm">确定</el-button>
   </span>
 </template>
</el-dialog>
 <!-- 添加地址 -->
</template>

<style scoped lang="scss">
.xtx-pay-checkout-page {
 margin-top: 20px;

 .wrapper {
   background: #fff;
   padding: 0 20px;

   .box-title {
     font-size: 16px;
     font-weight: normal;
     padding-left: 10px;
     line-height: 70px;
     border-bottom: 1px solid #f5f5f5;
   }

   .box-body {
     padding: 20px 0;
   }
 }
}

.address {
 border: 1px solid #f5f5f5;
 display: flex;
 align-items: center;

 .text {
   flex: 1;
   min-height: 90px;
   display: flex;
   align-items: center;

   .none {
     line-height: 90px;
     color: #999;
     text-align: center;
     width: 100%;
   }

   >ul {
     flex: 1;
     padding: 20px;

     li {
       line-height: 30px;

       span {
         color: #999;
         margin-right: 5px;

         >i {
           width: 0.5em;
           display: inline-block;
         }
       }
     }
   }

   >a {
     color: $xtxColor;
     width: 160px;
     text-align: center;
     height: 90px;
     line-height: 90px;
     border-right: 1px solid #f5f5f5;
   }
 }

 .action {
   width: 420px;
   text-align: center;

   .btn {
     width: 140px;
     height: 46px;
     line-height: 44px;
     font-size: 14px;

     &:first-child {
       margin-right: 10px;
     }
   }
 }
}

.goods {
 width: 100%;
 border-collapse: collapse;
 border-spacing: 0;

 .info {
   display: flex;
   text-align: left;

   img {
     width: 70px;
     height: 70px;
     margin-right: 20px;
   }

   .right {
     line-height: 24px;

     p {
       &:last-child {
         color: #999;
       }
     }
   }
 }

 tr {
   th {
     background: #f5f5f5;
     font-weight: normal;
   }

   td,
   th {
     text-align: center;
     padding: 20px;
     border-bottom: 1px solid #f5f5f5;

     &:first-child {
       border-left: 1px solid #f5f5f5;
     }

     &:last-child {
       border-right: 1px solid #f5f5f5;
     }
   }
 }
}

.my-btn {
 width: 228px;
 height: 50px;
 border: 1px solid #e4e4e4;
 text-align: center;
 line-height: 48px;
 margin-right: 25px;
 color: #666666;
 display: inline-block;

 &.active,
 &:hover {
   border-color: $xtxColor;
 }
}

.total {
 dl {
   display: flex;
   justify-content: flex-end;
   line-height: 50px;

   dt {
     i {
       display: inline-block;
       width: 2em;
     }
   }

   dd {
     width: 240px;
     text-align: right;
     padding-right: 70px;

     &.price {
       font-size: 20px;
       color: $priceColor;
     }
   }
 }
}

.submit {
 text-align: right;
 padding: 60px;
 border-top: 1px solid #f5f5f5;
}

.addressWrapper {
 max-height: 500px;
 overflow-y: auto;
}

.text {
 flex: 1;
 min-height: 90px;
 display: flex;
 align-items: center;

 &.item {
   border: 1px solid #f5f5f5;
   margin-bottom: 10px;
   cursor: pointer;

   &.active,
   &:hover {
     border-color: $xtxColor;
     background: lighten($xtxColor, 50%);
   }

   >ul {
     padding: 10px;
     font-size: 14px;
     line-height: 30px;
   }
 }
}
</style>

在这里插入图片描述

  • 生成订单功能实现
    • 业务需求
      • 调用接口生成id,并携带id跳转
      • 调用更新购物车列表接口,更新购物车状态
    • 步骤
      • 准备支付页
      • 封装生成订单接口
      • 点击按钮调用接口,得到id,携带id完成路由跳转

export const createOrderAPI = (data) =>{
    return request({
        url:'/member/order',
        method:'POST',
        data
    })
}
<script setup>
import { getCheckInfoAPI,createOrderAPI } from '@/apis/checkout.js'
import { useRouter } from 'vue-router';
import { onMounted,ref } from 'vue';
import { useCartStore } from '@/stores/cartStore';
const router =useRouter()
const cartStore = useCartStore()
const checkInfo = ref({})  // 订单对象
const curAddress = ref({})  // 地址对象
const getCheckInfo = async() => {
    const res = await getCheckInfoAPI()
    checkInfo.value = res.result
    const item = checkInfo.value.userAddresses.find(item => item.isDefault ===0)
    curAddress.value = item
}
onMounted(()=>getCheckInfo())

const showDialog = ref(false)

const activeAdress = ref({})
const switchAdress = (item) =>{
    activeAdress.value=item
}
const confirm = () =>{
    curAddress.value = activeAdress.value
    showDialog.value = false
    activeAdress.value= {}
}

const createOrder = async() =>{
    const 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
    })
    const orderId = res.result.id
    router.push({
        path:'/pay',
        query:{
            id:orderId
        }
    })
    cartStore.updateNewList()
}
</script>
<!-- 提交订单 -->
        <div class="submit">
          <el-button @click="createOrder" type="primary" size="large" >提交订单</el-button>
        </div>

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值