vue3 之 商城项目—购物车

购物车业务逻辑梳理拆解

1️⃣整个购物车的实现分为两个大分支,本地购物车操作和接口购物车操作
2️⃣由于购物车数据的特殊性,采取Pinia管理购物车列表数据并添加持久话缓存
在这里插入图片描述

本地购物车—加入购物车实现

在这里插入图片描述
stores/cartStore.js

// 封装购物车模块
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore = defineStore('cart', () => {// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCartconst addCart = (goods) => {console.log('添加', 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,
})

detail/index.vue

<script setup>
import DetailHot from './components/DetailHot.vue'
import { getDetail } from '@/apis/detail'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useCartStore } from '@/stores/cartStore'
const cartStore = useCartStore()
const goods = ref({})
const route = useRoute()
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}
onMounted(() => getGoods())// sku规格被操作时
let skuObj = {}
const skuChange = (sku) => {console.log(sku)skuObj = sku
}// count
const count = ref(1)
const countChange = (count) => {console.log(count)
}// 添加购物车
const addCart = () => {if (skuObj.skuId) {console.log(skuObj, cartStore.addCart)// 规则已经选择  触发actioncartStore.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('请选择规格')}
}</script><template><div class="xtx-goods-page"><div class="container" v-if="goods.details"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/${goods.categories[1].id}` }">{{ goods.categories[1].name }}</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[0].id}` }">{{goods.categories[0].name}}</el-breadcrumb-item><el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 图片预览区 --><XtxImageView :image-list="goods.mainPictures" /><!-- 统计数量 --><ul class="goods-sales"><li><p>销量人气</p><p> {{ goods.salesCount }}+ </p><p><i class="iconfont icon-task-filling"></i>销量人气</p></li><li><p>商品评价</p><p>{{ goods.commentCount }}+</p><p><i class="iconfont icon-comment-filling"></i>查看评价</p></li><li><p>收藏人气</p><p>{{ goods.collectCount }}+</p><p><i class="iconfont icon-favorite-filling"></i>收藏商品</p></li><li><p>品牌信息</p><p>{{ goods.brand.name }}</p><p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p></li></ul></div><div class="spec"><!-- 商品信息区 --><p class="g-name"> {{ goods.name }} </p><p class="g-desc">{{ goods.desc }} </p><p class="g-price"><span>{{ goods.oldPrice }}</span><span> {{ goods.price }}</span></p><div class="g-service"><dl><dt>促销</dt><dd>12月好物放送,App领券购买直降120</dd></dl><dl><dt>服务</dt><dd><span>无忧退货</span><span>快速退款</span><span>免费包邮</span><a href="javascript:;">了解详情</a></dd></dl></div><!-- sku组件 --><XtxSku :goods="goods" @change="skuChange" /><!-- 数据组件 --><el-input-number v-model="count" @change="countChange" /><!-- 按钮组件 --><div><el-button size="large" class="btn" @click="addCart">加入购物车</el-button></div></div></div><div class="goods-footer"><div class="goods-article"><!-- 商品详情 --><div class="goods-tabs"><nav><a>商品详情</a></nav><div class="goods-detail"><!-- 属性 --><ul class="attrs"><li v-for="item in goods.details.properties" :key="item.value"><span class="dt">{{ item.name }}</span><span class="dd">{{ item.value }}</span></li></ul><!-- 图片 --><img v-for="img in goods.details.pictures" :src="img" :key="img" alt=""></div></div></div><!-- 24热榜+专题推荐 --><div class="goods-aside"><!-- 24小时 --><DetailHot :hot-type="1" /><!----><DetailHot :hot-type="2" /></div></div></div></div></div></div>
</template><style scoped lang='scss'>
.xtx-goods-page {.goods-info {min-height: 600px;background: #fff;display: flex;.media {width: 580px;height: 600px;padding: 30px 50px;}.spec {flex: 1;padding: 30px 30px 30px 0;}}.goods-footer {display: flex;margin-top: 20px;.goods-article {width: 940px;margin-right: 20px;}.goods-aside {width: 280px;min-height: 1000px;}}.goods-tabs {min-height: 600px;background: #fff;}.goods-warn {min-height: 600px;background: #fff;margin-top: 20px;}.number-box {display: flex;align-items: center;.label {width: 60px;color: #999;padding-left: 10px;}}.g-name {font-size: 22px;}.g-desc {color: #999;margin-top: 10px;}.g-price {margin-top: 10px;span {&::before {content: "¥";font-size: 14px;}&:first-child {color: $priceColor;margin-right: 10px;font-size: 22px;}&:last-child {color: #999;text-decoration: line-through;font-size: 16px;}}}.g-service {background: #f5f5f5;width: 500px;padding: 20px 10px 0 10px;margin-top: 10px;dl {padding-bottom: 20px;display: flex;align-items: center;dt {width: 50px;color: #999;}dd {color: #666;&:last-child {span {margin-right: 10px;&::before {content: "•";color: $xtxColor;margin-right: 2px;}}a {color: $xtxColor;}}}}}.goods-sales {display: flex;width: 400px;align-items: center;text-align: center;height: 140px;li {flex: 1;position: relative;~li::after {position: absolute;top: 10px;left: 0;height: 60px;border-left: 1px solid #e4e4e4;content: "";}p {&:first-child {color: #999;}&:nth-child(2) {color: $priceColor;margin-top: 10px;}&:last-child {color: #666;margin-top: 10px;i {color: $xtxColor;font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;cursor: pointer;}}}}}
}.goods-tabs {min-height: 600px;background: #fff;nav {height: 70px;line-height: 70px;display: flex;border-bottom: 1px solid #f5f5f5;a {padding: 0 40px;font-size: 18px;position: relative;>span {color: $priceColor;font-size: 16px;margin-left: 10px;}}}
}.goods-detail {padding: 40px;.attrs {display: flex;flex-wrap: wrap;margin-bottom: 30px;li {display: flex;margin-bottom: 10px;width: 50%;.dt {width: 100px;color: #999;}.dd {flex: 1;color: #666;}}}>img {width: 100%;}
}.btn {margin-top: 20px;}.bread-container {padding: 25px 0;
}
</style>

本地购物车—头部购物车列表渲染

在这里插入图片描述
在这里插入图片描述
HeaderCart.vue

<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="cartStore.delCart(i.skuId)"></i></div></div><div class="foot"><div class="total"><p>{{ cartStore.allCount }} 件商品</p><p>&yen; {{ cartStore.allPrice.toFixed(2) }} </p></div><el-button size="large" type="primary" @click="$router.push('/cartlist')">去购物车结算</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>

本地购物车—头部购物车删除实现

在这里插入图片描述
stores/cartStore.js

 // 删除购物车const delCart = async (skuId) => {// 思路:// 1. 找到要删除项的下标值 - splice// 2. 使用数组的过滤方法 - filterconst idx = cartList.value.findIndex((item) => skuId === item.skuId)cartList.value.splice(idx, 1)}

HeaderCart.vue

  <i class="iconfont icon-close-new" @click="cartStore.delCart(i.skuId)"></i>

本地购物车—头部购物车统计计算

在这里插入图片描述
计算逻辑
1️⃣商品总数计算逻辑:商品列表中的所有商品count累加之和
2️⃣商品总价钱计算逻辑:商品列表中的所有商品的count*price累加之和
stores/cartStore.js

  // 计算属性// 1. 总的数量 所有项的count之和const allCount = computed(() => cartList.value.reduce((a, c) => a + c.count, 0))// 2. 总价 所有项的count*price之和const allPrice = computed(() => cartList.value.reduce((a, c) => a + c.count * c.price, 0))

本地购物车—列表购物车

在这里插入图片描述
在这里插入图片描述
cartList/index.vue

<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 :model-value="cartStore.isAll" @change="allCheck" /></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 :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" /></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">{{ cartStore.allCount }} 件商品,已选择 {{ cartStore.selectedCount }} 件,商品合计:<span class="red">¥ {{ cartStore.selectedPrice.toFixed(2) }} </span></div><div class="total"><el-button size="large" type="primary" @click="$router.push('/checkout')">下单结算</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>

router.js

// createRouter:创建router实例对象
// createWebHistory:创建history模式的路由
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
import SubCategory from '@/views/SubCategory/index.vue'
import Detail from '@/views/Detail/index.vue'
import CartList from '@/views/CartList/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),// path和component对应关系的位置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: '/login',component: Login}],// 路由滚动行为定制scrollBehavior () {return {top: 0}}
})export default router

列表购物车—单选功能

核心思路
单选的核心思路就是始终把单选框的状态和Pinia中的store对应的状态保持同步
注意事项
v-model双向绑定指令不方便进行命令式的操作(因为后续还需要调用接口),所以把v-model回退到一般模式,也就是model-value和@change的配合实现
在这里插入图片描述

cartStore.js

const singleCheck = (skuId, selected) => {// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selectedconst item = cartList.value.find((item) => item.skuId === skuId)item.selected = selected
}

cartList/index.vue

// 单选回调
const singleCheck = (i, selected) => {console.log(i, selected)// store cartList 数组 无法知道要修改谁的选中状态?// 除了selected补充一个用来筛选的参数 - skuIdcartStore.singleCheck(i.skuId, selected)
}
<template><td><!-- 单选框 --><el-checkbox :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" /></td>
</template>

列表购物车—全选

核心思路
1️⃣操作单选决定全选:只有当cartList中的所有项都为true时,全选状态才为true
2️⃣操作全选决定单选:cartList中的所有项的selected都要跟着一起变
在这里插入图片描述
cartStore.js

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

cartList/index.vue

const allCheck = (selected) => {cartStore.allCheck(selected)
}<el-checkbox :model-value="cartStore.isAll" @change="allCheck" />

列表购物车—统计数据实现

在这里插入图片描述
计算逻辑
1️⃣已选择数量=cartList中所有selected字段为true项的count之和
2️⃣商品合计=cartList中所有selected字段为true项的count*price之和

cartStore.js

// 3. 已选择数量const selectedCount = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count, 0))// 4. 已选择商品价钱合计const selectedPrice = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count * c.price, 0))

cartList/index.vue

 <!-- 操作栏 --><div class="action"><div class="batch">{{ cartStore.allCount }} 件商品,已选择 {{ cartStore.selectedCount }} 件,商品合计:<span class="red">¥ {{ cartStore.selectedPrice.toFixed(2) }} </span></div><div class="total"><el-button size="large" type="primary" @click="$router.push('/checkout')">下单结算</el-button></div></div>

接口购物车—加入购物车

在这里插入图片描述
结论
到目前为止,购物车在非登陆状态下的各种操作都已经ok了,包括action的封装,触发,参数传递,剩下的事情就是action中做登录状态的分支判断,补充登录状态下的接口操作逻辑即可
在这里插入图片描述
cartStore.js

import { defineStore } from 'pinia'
import { useUserStore } from './userStore'
import { computed, ref } from 'vue'
import { insertCartAPI, findNewCartListAPI, delCartAPI  } from '@/apis/cart'
export const useCartStore = defineStore('cart', () => {const userStore = useUserStore()// 1. 定义state - cartListconst cartList = ref([])const isLogin = computed(() => userStore.userInfo.token)// 获取最新购物车列表actionconst updateNewList = async () => {const res = await findNewCartListAPI()cartList.value = res.result}const addCart = async (goods) => {const { skuId, count } = goods// 登录if (isLogin.value) {// 登录之后的加入购车逻辑await insertCartAPI({ skuId, count })updateNewList()} else {// 未登录const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 找到了item.count++} else {// 没找到cartList.value.push(goods)}}}
}, {persist: true,
})

接口购物车—删除购物车

在这里插入图片描述
cartStore.js

 // 删除购物车const delCart = async (skuId) => {if (isLogin.value) {// 调用接口实现接口购物车中的删除功能await delCartAPI([skuId])updateNewList()} else {// 思路:// 1. 找到要删除项的下标值 - splice// 2. 使用数组的过滤方法 - filterconst idx = cartList.value.findIndex((item) => skuId === item.skuId)cartList.value.splice(idx, 1)}}

退出登陆—清空购物车列表

业务需求
在用户退出登陆时,清除用户信息同时也要把购物车数据清空
在这里插入图片描述
cartStore.js

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

userStore.js

  import { useCartStore } from './cartStore'const cartStore = useCartStore()// 退出时清除用户信息const clearUserInfo = () => {userInfo.value = {}// 执行清除购物车的actioncartStore.clearCart()}

合并本地购物车到服务器

用户在非登陆时所进行的操作,登陆后把本地的购物车数据和服务端购物车数据进行合并
在这里插入图片描述
实现步骤
在这里插入图片描述
userStore.js

 // 2. 定义获取接口数据的action函数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()}

完整代码

userStore.js

// 管理用户数据相关
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()// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取接口数据的action函数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 = {}// 执行清除购物车的actioncartStore.clearCart()}// 3. 以对象的格式把state和action returnreturn {userInfo,getUserInfo,clearUserInfo}
}, {persist: true,
})

cartStore.js

// 封装购物车模块import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useUserStore } from './userStore'
import { insertCartAPI, findNewCartListAPI, delCartAPI } from '@/apis/cart'
export const useCartStore = defineStore('cart', () => {const userStore = useUserStore()const isLogin = computed(() => userStore.userInfo.token)// 1. 定义state - cartListconst cartList = ref([])// 获取最新购物车列表actionconst updateNewList = async () => {const res = await findNewCartListAPI()cartList.value = res.result}// 2. 定义action - addCartconst addCart = async (goods) => {const { skuId, count } = goodsif (isLogin.value) {// 登录之后的加入购车逻辑await insertCartAPI({ skuId, count })updateNewList()} 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)}}}// 删除购物车const delCart = async (skuId) => {if (isLogin.value) {// 调用接口实现接口购物车中的删除功能await delCartAPI([skuId])updateNewList()} else {// 思路:// 1. 找到要删除项的下标值 - splice// 2. 使用数组的过滤方法 - filterconst idx = cartList.value.findIndex((item) => skuId === item.skuId)cartList.value.splice(idx, 1)}}// 清除购物车const clearCart = () => {cartList.value = []}// 单选功能const singleCheck = (skuId, selected) => {// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selectedconst item = cartList.value.find((item) => item.skuId === skuId)item.selected = selected}// 全选功能const allCheck = (selected) => {// 把cartList中的每一项的selected都设置为当前的全选框状态cartList.value.forEach(item => item.selected = selected)}// 计算属性// 1. 总的数量 所有项的count之和const allCount = computed(() => cartList.value.reduce((a, c) => a + c.count, 0))// 2. 总价 所有项的count*price之和const allPrice = computed(() => cartList.value.reduce((a, c) => a + c.count * c.price, 0))// 3. 已选择数量const selectedCount = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count, 0))// 4. 已选择商品价钱合计const selectedPrice = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count * c.price, 0))// 是否全选const isAll = computed(() => cartList.value.every((item) => item.selected))return {cartList,allCount,allPrice,isAll,selectedCount,selectedPrice,clearCart,addCart,delCart,singleCheck,allCheck,updateNewList}
}, {persist: true,
})

cartList/index.vue

<script setup>
import { useCartStore } from '@/stores/cartStore'
const cartStore = useCartStore()// 单选回调
const singleCheck = (i, selected) => {console.log(i, selected)// store cartList 数组 无法知道要修改谁的选中状态?// 除了selected补充一个用来筛选的参数 - skuIdcartStore.singleCheck(i.skuId, selected)
}
const allCheck = (selected) => {cartStore.allCheck(selected)
}
</script><template><div class="xtx-cart-page"><div class="container m-top-20"><div class="cart"><table><thead><tr><th width="120"><el-checkbox :model-value="cartStore.isAll" @change="allCheck" /></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 :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" /></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">{{ cartStore.allCount }} 件商品,已选择 {{ cartStore.selectedCount }} 件,商品合计:<span class="red">¥ {{ cartStore.selectedPrice.toFixed(2) }} </span></div><div class="total"><el-button size="large" type="primary" @click="$router.push('/checkout')">下单结算</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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/469694.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024.2.10 HCIA - Big Data笔记

1. 大数据发展趋势与鲲鹏大数据大数据时代大数据的应用领域企业所面临的挑战和机遇华为鲲鹏解决方案2. HDFS分布式文件系统和ZooKeeperHDFS分布式文件系统HDFS概述HDFS相关概念HDFS体系架构HDFS关键特性HDFS数据读写流程ZooKeeper分布式协调服务ZooKeeper概述ZooKeeper体系结构…

【AI视野·今日Sound 声学论文速览 第四十九期】Wed, 17 Jan 2024

AI视野今日CS.Sound 声学论文速览 Wed, 17 Jan 2024 Totally 23 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Sound Papers From Coarse to Fine: Efficient Training for Audio Spectrogram Transformers Authors Jiu Feng, Mehmet Hamza Erol, Joon Son Chung,…

【初学者必看】迈入Midjourney的艺术世界:轻松掌握Midjourney的注册与订阅!

文章目录 前言一、Midjourney是什么二、Midjourney注册三、新建自己的服务器四、开通订阅 前言 AI绘画即指人工智能绘画&#xff0c;是一种计算机生成绘画的方式。是AIGC应用领域内的一大分支。 AI绘画主要分为两个部分&#xff0c;一个是对图像的分析与判断&#xff0c;即…

测试开发-2-概念篇

文章目录 衡量软件测试结果的依据—需求1.需求的概念2.从软件测试人员角度看需求3.为什么需求对软件测试人员如此重要4.如何才可以深入理解被测试软件的需求5.测试用例的概念6.软件错误&#xff08;BUG&#xff09;的概念7.开发模型和测试模型8.软件的生命周期9.瀑布模型&#…

Python算法题集_对称二叉树

Python算法题集_对称二叉树 题101&#xff1a;对称二叉树1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【DFS递归】2) 改进版一【BFS迭代】3) 改进版二【BFS迭代deque】 4. 最优算法 本文为Python算法题集之一的代码示例 题101&#xff1a;对…

Requests教程-10-Session会话对象

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节中&#xff0c;我们学习了requests的cookies参数使用方法&#xff0c;本小节我们讲解一下requests库中的session会话对象。 在requests中&#xff0c;如果直接利用get()或post()等方法的确可以做到模拟网…

ng : 无法加载文件 C:\Program Files\nodejs\node_global\ng.ps1, 因为在此系统上禁止运行脚本

ng : 无法加载文件 C:\Program Files\nodejs\node_global\ng.ps1&#xff0c;因为在此系统上禁止运行脚本 今天在VSCode中运行ng serve --port 8081运行基于Angular的项目时&#xff0c;报错了&#xff0c;错误如下图所示&#xff1a; 解决方法&#xff1a; 按照下图的5步即…

UnityShader——04渲染流水

渲染流水 GPU应用阶段 把数据加载到显存中设置渲染状态调用DrawCall 将渲染所需数据从硬盘加载到内存中&#xff0c;网格纹理等数据又被加载到显存中&#xff08;一般加载到显存后内存中的数据就会被移除&#xff09; 这些状态定义了场景中的网格是怎样被渲染的。例如&#xf…

springboot191教师工作量管理系统

简介 【 毕设 源码 推荐 javaweb 项目】 基于 springbootvue 的教师工作量管理系统&#xff08;springboot191&#xff09; 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后…

Leetcode 452. 用最少数量的箭引爆气球435. 无重叠区间

class Solution {public int findMinArrowShots(int[][] points) {Arrays.sort(points,(o1,o2)->Integer.compare(o1[0], o2[0]));int count1;//箭的数量for(int i1;i<points.length;i) {if(points[i][0]>points[i-1][1]) {count;//边界没重合&#xff0c;又需要一支箭…

文件操作详解

文章目录 目录1. 为什么使用文件2. 什么是文件2.1 程序文件2.2 数据文件2.3 文件名 3. 文件的打开和关闭3.1 文件指针3.2 文件的打开和关闭 4. 文件的顺序读写5. 通讯录的改造6. 文件的随机读写6.1 fseek6.2 ftell6.3 rewind 7. 文本文件和二进制文件8. 文件读取结束的判定9. 文…

8.JS中的== 操作符的强制类型转换规则

对于 来说&#xff0c;如果对比双方的类型不一样&#xff0c;就会进行类型转换。假如对比 x 和 y 是否相同&#xff0c;就会进行如下判断流程&#xff1a; 首先会判断两者类型是否相同&#xff0c;类型相同的话就比较两者的大小&#xff1b;类型不相同的话&#xff0c;就会进…