【学习打卡】第12天 【2022版】Vue3 系统入门与项目实战第十二讲
课程名称: 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>
点击查看更多内容
为 TA 点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦