课程名称: 2022持续升级 Vue3 从入门到实战 掌握完整知识体系
课程章节: 核心购物链路开发
主讲老师: Dell
今天学习的内容包括:
如何开发核心购物的链路
src/views/orderConfirmation/Order.vue
<template> <div class="order"> <div class="order__price">实付金额 <b>¥{{calculations.price}}</b></div> <div class="order__btn" @click="() => handleShowConfirmChange(true)">提交订单</div> </div> <div class="mask" v-show="showConfirm" @click="() => handleShowConfirmChange(false)" > <div class="mask__content" @click.stop> <h3 class="mask__content__title">确认要离开收银台?</h3> <p class="mask__content__desc">请尽快完成支付,否则将被取消</p> <div class="mask__content__btns"> <div class="mask__content__btn mask__content__btn--first" @click="() => handleConfirmOrder(true)" >取消订单</div> <div class="mask__content__btn mask__content__btn--last" @click="() => handleConfirmOrder(false)" >确认支付</div> </div> </div> </div> </template> <script> import { ref } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useStore } from 'vuex' import { post } from '../../utils/request' import { useCommonCartEffect } from '../../effects/cartEffects' // 下单相关逻辑 const useMakeOrderEffect = (shopId, shopName, productList) => { const router = useRouter() const store = useStore() const handleConfirmOrder = async (isCanceled) => { const products = [] for(let i in productList.value) { const product = productList.value[i] products.push({id: parseInt(product._id, 10), num: product.count}) } try { const result = await post('/api/order', { addressId: 1, shopId, shopName: shopName.value, isCanceled, products }) if (result?.errno === 0) { store.commit('clearCartData', shopId) router.push({ name: 'OrderList' }) } } catch (e) { // 提示下单失败 } } return { handleConfirmOrder } } // 蒙层展示相关的逻辑 const useShowMaskEffect = () => { const showConfirm = ref(false) const handleShowConfirmChange = (status) => { showConfirm.value = status } return { showConfirm, handleShowConfirmChange } } export default { name: 'Order', setup() { const route = useRoute() const shopId = parseInt(route.params.id, 10) const { calculations, shopName, productList } = useCommonCartEffect(shopId) const { handleConfirmOrder } = useMakeOrderEffect(shopId, shopName, productList) const { showConfirm, handleShowConfirmChange } = useShowMaskEffect() return { showConfirm, handleShowConfirmChange, calculations, handleConfirmOrder } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; .order { position: absolute; left: 0; right: 0; bottom: 0; display: flex; height: .49rem; line-height: .49rem; background: $bgColor; &__price { flex: 1; text-indent: .24rem; font-size: .14rem; color: $content-fontcolor; } &__btn { width: .98rem; background: #4FB0F9; color: #fff; text-align: center; font-size: .14rem; } } .mask { z-index: 1; position: absolute; left: 0; right: 0; bottom: 0; top: 0; background: rgba(0,0,0,0.50); &__content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 3rem; height: 1.56rem; background: #FFF; text-align: center; border-radius: .04rem; &__title { margin: .24rem 0 0 0; line-height: .26rem; font-size: .18rem; color: #333; } &__desc { margin: .08rem 0 0 0; font-size: .14rem; color: #666666; } &__btns { display: flex; margin: .24rem .58rem; } &__btn { flex: 1; width: .8rem; line-height: .32rem; border-radius: .16rem; font-size: .14rem; &--first { margin-right: .12rem; border: .01rem solid #4FB0F9; color: #4FB0F9; } &--last { margin-left: .12rem; background: #4FB0F9; color: #fff; } } } } </style>
src/views/orderConfirmation/OrderConfirmation.vue
<template> <div class="wrapper"> <TopArea /> <ProductList /> <Order /> </div> </template> <script> import TopArea from './TopArea' import ProductList from './ProductList' import Order from './Order' export default { name: 'OrderConfirmation', components: { TopArea, ProductList, Order }, } </script> <style lang="scss" scoped> .wrapper { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background-color: #eee; overflow-y: scroll; } </style>
src/views/orderConfirmation/ProductList.vue
<template> <div class="products"> <div class="products__title"> {{shopName}} </div> <div class="products__wrapper"> <div class="products__list"> <div v-for="item in productList" :key="item._id" class="products__item" > <img class="products__item__img" :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="item.imgUrl" /> <div class="products__item__detail"> <h4 class="products__item__title">{{item.name}}</h4> <p class="products__item__price"> <span> <span class="products__item__yen">¥ </span> {{item.price}} x {{item.count}} </span> <span class="products__item__total"> <span class="products__item__yen">¥ </span> {{(item.price * item.count).toFixed(2)}} </span> </p> </div> </div> </div> </div> </div> </template> <script> import { useRoute } from 'vue-router' import { useCommonCartEffect } from '../../effects/cartEffects' export default { name: 'ProductList', setup() { const route = useRoute() const shopId = route.params.id const { shopName, productList } = useCommonCartEffect(shopId) return { shopName, productList } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .products { margin: .16rem .18rem .1rem .18rem; background: $bgColor; &__title { padding: .16rem; font-size: .16rem; color: $content-fontcolor; } &__wrapper { overflow-y: scroll; margin: 0 .18rem; position: absolute; left: 0; right: 0; bottom: .6rem; top: 2.6rem; } &__list { background: $bgColor; } &__item { position: relative; display: flex; padding: 0 .16rem 0.16rem .16rem; &__img { width: .46rem; height: .46rem; margin-right: .16rem; } &__detail { flex: 1; } &__title { margin: 0; line-height: .2rem; font-size: .14rem; color: $content-fontcolor; @include ellipsis; } &__price { display: flex; margin: .06rem 0 0 0; line-height: .2rem; font-size: .14rem; color: $hightlight-fontColor; } &__total { flex: 1; text-align: right; color: $dark-fontColor; } &__yen { font-size: .12rem; } } } </style>
src/views/orderConfirmation/TopArea.vue
<template> <div class="top"> <div class="top__header"> <div class="iconfont top__header__back" @click="handleBackClick" ></div> 确认订单 </div> <div class="top__receiver"> <div class="top__receiver__title">收货地址</div> <div class="top__receiver__address">北京理工大学国防科技园2号楼10层</div> <div class="top__receiver__info"> <span class="top__receiver__info__name">瑶妹(先生)</span> <span class="top__receiver__info__name">18911024266</span> </div> <div class="iconfont top__receiver__icon"></div> </div> </div> </template> <script> import { useRouter } from 'vue-router' export default { name: 'TopArea', setup() { const router = useRouter() const handleBackClick = () => { router.back() } return { handleBackClick } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; .top { position: relative; height: 1.96rem; background-size: 100% 1.59rem; background-image: linear-gradient(0deg, rgba(0,145,255,0.00) 4%, #0091FF 50%); background-repeat: no-repeat; &__header { position: relative; padding-top: .26rem; line-height: .24rem; color: $bgColor; text-align: center; font-size: .16rem; &__back { position: absolute; left: .18rem; font-size: .22rem; } } &__receiver { position: absolute; left: .18rem; right: .18rem; bottom: 0; height: 1.11rem; background: $bgColor; border-radius: .04rem; &__title { line-height: .22rem; padding: .16rem 0 .14rem .16rem; font-size: .16rem; color: $content-fontcolor; } &__address { line-height: .2rem; padding: 0 .4rem 0 .16rem; font-size: .14rem; color: $content-fontcolor; } &__info { padding: .06rem 0 0 .16rem; &__name { margin-right: .06rem; line-height: .18rem; font-size: .12rem; color: $medium-fontColor; } } &__icon { transform: rotate(180deg); position: absolute; right: .16rem; top: .5rem; color: $medium-fontColor; font-size: .2rem; } } } </style>
src/effects/cartEffects.js
import { computed } from 'vue' import { useStore } from 'vuex' // 购物车相关逻辑 export const useCommonCartEffect = (shopId) => { const store = useStore() const cartList = store.state.cartList; const changeCartItemInfo = (shopId, productId, productInfo, num) => { store.commit('changeCartItemInfo', { shopId, productId, productInfo, num }) } const productList = computed(() => { const productList = cartList[shopId]?.productList || {} const notEmptyProductList = {} for(let i in productList) { const product = productList[i] if(product.count > 0 ) { notEmptyProductList[i] = product } } return notEmptyProductList }) const shopName = computed(() => { const shopName = cartList[shopId]?.shopName || '' return shopName }) const calculations = computed(() => { const productList = cartList[shopId]?.productList const result = { total: 0, price: 0, allChecked: true} if(productList) { for(let i in productList) { const product = productList[i] result.total += product.count if(product.check) { result.price += (product.count * product.price) } if(product.count > 0 && !product.check) { result.allChecked = false } } } result.price = result.price.toFixed(2) return result }) return { cartList, shopName, productList, calculations, changeCartItemInfo } }
src/compnents/Docker.vue
<template> <div class="docker"> <div v-for="(item, index) in dockerList" :class="{'docker__item': true, 'docker__item--active': index === currentIndex}" :key="item.icon" > <router-link :to='item.to'> <div class="iconfont" v-html="item.icon" /> <div class="docker__title">{{item.text}}</div> </router-link> </div> </div> </template> <script> export default { name: 'Docker', props: ['currentIndex'], setup() { const dockerList = [ {icon: '', text: '首页', to: {name: 'Home'}}, {icon: '', text: '购物车', to: {name: 'CartList'}}, {icon: '', text: '订单', to: {name: 'OrderList'}}, {icon: '', text: '我的', to: {name: 'Home'}}, ]; return { dockerList } } } </script> <style lang="scss" scoped> @import '../style/viriables.scss'; .docker { display: flex; box-sizing: border-box; position: absolute; padding: 0 .18rem; left: 0; bottom: 0; width: 100%; height: .49rem; border-top: .01rem solid $content-bgColor; &__item { flex: 1; text-align: center; a { color: $content-fontcolor; text-decoration: none; } .iconfont { margin: .07rem 0 .02rem 0; font-size: .18rem; } &--active { a { color: #1FA4FC; } } } &__title { font-size: .2rem; transform: scale(.5, .5); transform-origin: center top; } } </style>
src/views/shop/Cart.vue
<template> <div class="mask" v-if="showCart && calculations.total > 0" @click="handleCartShowChange" /> <div class="cart"> <div class="product" v-if="showCart && calculations.total > 0"> <div class="product__header"> <div class="product__header__all" @click="() => setCartItemsChecked(shopId)" > <span class="product__header__icon iconfont" v-html="calculations.allChecked ? '': ''" > </span> 全选 </div> <div class="product__header__clear"> <span class="product__header__clear__btn" @click="() => cleanCartProducts(shopId)" >清空购物车</span> </div> </div> <div v-for="item in productList" :key="item._id" class="product__item" > <div class="product__item__checked iconfont" v-html="item.check ? '': ''" @click="() => changeCartItemChecked(shopId, item._id)" /> <img class="product__item__img" :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="item.imgUrl" /> <div class="product__item__detail"> <h4 class="product__item__title">{{item.name}}</h4> <p class="product__item__price"> <span class="product__item__yen">¥</span>{{item.price}} <span class="product__item__origin">¥{{item.oldPrice}}</span> </p> </div> <div class="product__number"> <span class="product__number__minus" @click="() => { changeCartItemInfo(shopId, item._id, item, -1) }" >-</span> {{item.count || 0}} <span class="product__number__plus" @click="() => { changeCartItemInfo(shopId, item._id, item, 1) }" >+</span> </div> </div> </div> <div class="check"> <div class="check__icon"> <img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="http://www.dell-lee.com/imgs/vue3/basket.png" class="check__icon__img" @click="handleCartShowChange" /> <div class="check__icon__tag">{{calculations.total}}</div> </div> <div class="check__info"> 总计:<span class="check__info__price">¥ {{calculations.price}}</span> </div> <div class="check__btn" v-show="calculations.total > 0"> <router-link :to="{path: `/orderConfirmation/${shopId}`}"> 去结算 </router-link> </div> </div> </div> </template> <script> import { ref } from 'vue' import { useStore } from 'vuex' import { useRoute } from 'vue-router' import { useCommonCartEffect } from '../../effects/cartEffects' // 获取购物车信息逻辑 const useCartEffect = (shopId) => { const store = useStore() const { productList, calculations, changeCartItemInfo } = useCommonCartEffect(shopId) const changeCartItemChecked = (shopId, productId) => { store.commit('changeCartItemChecked', {shopId, productId}) } const cleanCartProducts = (shopId) => { store.commit('cleanCartProducts', { shopId }) } const setCartItemsChecked = (shopId) => { store.commit('setCartItemsChecked', { shopId }) } return { calculations, productList, cleanCartProducts, changeCartItemInfo, changeCartItemChecked, setCartItemsChecked, } } // 展示隐藏购物车逻辑 const toggleCartEffect = () => { const showCart = ref(false) const handleCartShowChange = () => { showCart.value = !showCart.value; } return { showCart, handleCartShowChange} } export default { name: 'Cart', setup() { const route = useRoute(); const shopId = route.params.id; const { calculations, productList, cleanCartProducts, changeCartItemInfo, changeCartItemChecked, setCartItemsChecked } = useCartEffect(shopId) const { showCart, handleCartShowChange } = toggleCartEffect() return { calculations, shopId, productList, cleanCartProducts, changeCartItemInfo, changeCartItemChecked, setCartItemsChecked, showCart, handleCartShowChange } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .mask { position: fixed; left: 0; right: 0; bottom: 0; top: 0; background: rgba(0, 0, 0, .5); z-index: 1; } .cart { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; background: $bgColor; } .product { overflow-y: scroll; flex: 1; background: $bgColor; &__header { display: flex; line-height: .52rem; border-bottom: 1px solid $content-bgColor; font-size: .14rem; color: $content-fontcolor; &__all { width: .64rem; margin-left: .18rem; } &__icon { display: inline-block; margin-right: .1rem; vertical-align: top; color: $btn-bgColor; font-size: .2rem; } &__clear { flex: 1; margin-right: .16rem; text-align: right; &__btn { display: inline-block; } } } &__item { position: relative; display: flex; padding: .12rem 0; margin: 0 .16rem; border-bottom: .01rem solid $content-bgColor; &__checked { line-height: .5rem; margin-right: .2rem; color: $btn-bgColor; font-size: .2rem; } &__detail { overflow: hidden; } &__img { width: .46rem; height: .46rem; margin-right: .16rem; } &__title { margin: 0; line-height: .2rem; font-size: .14rem; color: $content-fontcolor; @include ellipsis; } &__price { margin: .06rem 0 0 0; line-height: .2rem; font-size: .14rem; color: $hightlight-fontColor; } &__yen { font-size: .12rem; } &__origin { margin-left: .06rem; line-height: .2rem; font-size: .12rem; color: $light-fontColor; text-decoration: line-through; } .product__number { position: absolute; right: 0; bottom: .26rem; &__minus, &__plus { display: inline-block; width: .2rem; height: .2rem; line-height: .16rem;; border-radius: 50%; font-size: .2rem; text-align: center; } &__minus { border: .01rem solid $medium-fontColor; color: $medium-fontColor; margin-right: .05rem; } &__plus { background: $btn-bgColor; color: $bgColor; margin-left: .05rem; } } } } .check { display: flex; height: .49rem; border-top: .01rem solid $content-bgColor; line-height: .49rem; &__icon { position: relative; width: .84rem; &__img { display: block; margin: .12rem auto; width: .28rem; height: .26rem; } &__tag { position: absolute; left: .46rem; top: .04rem; padding: 0 .04rem; min-width: .2rem; height: .2rem; line-height: .2rem; background-color: $hightlight-fontColor; border-radius: .1rem; font-size: .12rem; text-align: center; color: #fff; transform: scale(.5); transform-origin: left center; } } &__info { flex: 1; color: $content-fontcolor; font-size: .12rem; &__price { line-height: .49rem; color: $hightlight-fontColor; font-size: .18rem; } } &__btn { width: .98rem; background-color: #4FB0F9; text-align: center; font-size: .14rem; a { color: $bgColor; text-decoration: none; } } } </style>
src/views/orderList/OrderList.vue
<template> <div class="wrapper"> <div class="title">我的订单</div> <div class="orders"> <div class="order" v-for="(item, index) in list" :key="index" > <div class="order__title"> {{item.shopName}} <span class="order__status"> {{item.isCanceled ? '已取消' : '已下单'}} </span> </div> <div class="order__content"> <div class="order__content__imgs"> <template v-for="(innerItem, innerIndex) in item.products" :key="innerIndex" > <img class="order__content__img" :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="innerItem.product.img" v-if="innerIndex <= 3" /> </template> </div> <div class="order__content__info"> <div class="order__content__price">¥ {{item.totalPrice}}</div> <div class="order__content__count">共 {{item.totalNumber}} 件</div> </div> </div> </div> </div> </div> <Docker :currentIndex="2"/> </template> <script> import { reactive, toRefs } from 'vue' import { get } from '../../utils/request' import Docker from '../../components/Docker' // 处理订单列表逻辑 const useOrderListEffect = () => { const data = reactive({ list:[]}) const getNearbyList = async () => { const result = await get('/api/order') if (result?.errno === 0 && result?.data?.length) { const orderList = result.data orderList.forEach((order) => { const products = order.products || [] let totalPrice = 0 let totalNumber = 0 products.forEach((productItem) => { totalNumber += (productItem?.orderSales || 0) totalPrice += ((productItem?.product?.price * productItem?.orderSales) || 0) }) order.totalPrice = totalPrice order.totalNumber = totalNumber }) data.list = result.data } } getNearbyList() const { list } = toRefs(data) return { list } } export default { name: 'OrderList', components: { Docker }, setup() { const { list } = useOrderListEffect() return { list } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; .wrapper { overflow-y: auto; position: absolute; left: 0; top: 0; bottom: .5rem; right: 0; background: rgb(248, 248,248); } .title { line-height: .44rem; background: $bgColor; font-size: .16rem; color: $content-fontcolor; text-align: center; } .order { margin: .16rem .18rem; padding: .16rem; background: $bgColor; &__title { margin-bottom: .16rem; line-height: .22rem; font-size: .16rem; color: $content-fontcolor; } &__status { float: right; font-size: .14rem; color: $light-fontColor; } &__content { display: flex; &__imgs { flex: 1; } &__img { width: .4rem; height: .4rem; margin-right: .12rem; } &__info { width: .7rem; } &__price { margin-bottom: .04rem; line-height: .2rem; font-size: .14rem; color: $hightlight-fontColor; text-align: right; } &__count { line-height: .14rem; font-size: .12rem; color: $content-fontcolor; text-align: right; } } } </style>