课程名称: 2022持续升级 Vue3 从入门到实战 掌握完整知识体系
课程章节: 【2022加餐】购物地址管理功能实现
主讲老师: Dell
今天学习的内容包括:
如何实现购物地址的管理功能
src/views/address/Address.vue
<template> <div class="wrapper"> <div class="title"> 我的地址 <span class="title__create"> <router-link to='/addressEdit'>新建</router-link> </span> </div> <div class="empty" v-if="addressList.length === 0" > 暂无地址信息 </div> <div class="address" v-if="addressList.length > 0" > <div class="address__item" v-for="address in addressList" :key="address._id" @click="() => handleAddressClick(address._id)" > <p class="address__item__basic"> {{address.name}} <span class="address__item__phone">{{address.phone}}</span> </p> <p class="address__item__address"> {{address.city}}{{address.department}}{{address.houseNumber}} </p> <div class="iconfont"></div> </div> </div> </div> <Docker :currentIndex="3"/> </template> <script> import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { get } from '../../utils/request'; import Docker from '../../components/Docker'; // 地址列表获取逻辑 const useAddressListEffect = () => { const addressList = ref([]); const getAddressList = async () => { const result = await get('/api/user/address') if (result?.errno === 0 && result?.data?.length) { addressList.value = result.data; } } return { addressList, getAddressList }; } export default { name: 'Address', components: { Docker }, setup() { const router = useRouter(); const { addressList, getAddressList } = useAddressListEffect(); getAddressList(); const handleAddressClick = (id) => { router.push(`/addressEdit?id=${id}`); }; return { addressList, handleAddressClick } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .wrapper { overflow-y: auto; @include fix-content; background: $darkBgColor; } .title { position: relative; @include title; &__create { position: absolute; right: .18rem; font-size: .14rem; a { text-decoration: none; color: $content-fontcolor; } } } .address { margin: .16rem .18rem 0 .18rem; &__item { position: relative; box-sizing: border-box; padding: .18rem .63rem .18rem .16rem; margin-bottom: .16rem; background: $bgColor; border-radius: .04rem; &__basic { line-height: .2rem; margin: 0; font-size: .14rem; color: $light-fontColor; } &__phone { margin-left: .66rem; } &__address { line-height: .2rem; margin: .08rem 0 0 0; font-size: .14rem; color: $content-fontcolor; } } .iconfont { transform: rotate(180deg); position: absolute; right: .16rem; top: .44rem; color: $light-fontColor; font-size: .2rem; } } .empty { @include empty; } </style>
src/views/addressEdit/AddressEdit.vue
<template> <div class="wrapper"> <Toast v-if="show" :message="toastMessage"/> <div class="title"> <div class="iconfont" @click="handleBackClick">  </div> {{isEdit ? '编辑' : '新建'}}地址 <span class="title__save" @click="handleSaveClick" >保存</span> </div> <div class="content"> <div class="content__item"> <span class="content__item__label">所在城市:</span> <input class="content__item__input" placeholder="请输入所在城市" v-model="city" /> </div> <div class="content__item"> <span class="content__item__label">小区/大厦/学校:</span> <input class="content__item__input" placeholder="请输入小区/大厦/学校" v-model="department" /> </div> <div class="content__item"> <span class="content__item__label">楼号-门牌号:</span> <input class="content__item__input" placeholder="请输入楼号-门牌号" v-model="houseNumber" /> </div> <div class="content__item"> <span class="content__item__label">收货人:</span> <input class="content__item__input" placeholder="请输入收货人" v-model="name" /> </div> <div class="content__item"> <span class="content__item__label">联系电话:</span> <input class="content__item__input" placeholder="请输入联系电话" v-model="phone" /> </div> </div> </div> </template> <script> import { onBeforeMount, ref } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { post } from '../../utils/request'; import Toast, { useToastEffect } from '../../components/Toast.vue'; // 点击回退逻辑 const useBackRouterEffect = () => { const router = useRouter() const handleBackClick = () => { router.back() } return { router, handleBackClick } } export default { name: 'AddressEdit', components: { Toast }, setup() { const route = useRoute(); const id = route.query.id; const city = ref(''); const department = ref(''); const houseNumber = ref(''); const name = ref(''); const phone = ref(''); const { show, toastMessage, showToast } = useToastEffect() const { router, handleBackClick } = useBackRouterEffect(); onBeforeMount(async () => { if(id) { const result = await post(`/api/user/address/${id}`) if (result?.errno === 0) { const data = result.data; city.value = data.city; department.value = data.department; houseNumber.value = data.houseNumber; name.value = data.name; phone.value = data.phone; } } }); const handleSaveClick = async () => { if( !city.value || !department.value || !houseNumber.value || !name.value || !phone.value ) { showToast('所有内容必填') }else { if(id) { const result = await post(`/api/user/address/${id}`, { city: city.value, department: department.value, houseNumber: houseNumber.value, name: name.value, phone: phone.value, }) if (result?.errno === 0) { router.back(); } }else { const result = await post('/api/user/address', { city: city.value, department: department.value, houseNumber: houseNumber.value, name: name.value, phone: phone.value, }) if (result?.errno === 0) { router.back(); } } } } return { city, department, houseNumber, name, phone, show, toastMessage, isEdit: !!id, handleBackClick, handleSaveClick, }; } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .wrapper { overflow: scroll; position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: $darkBgColor; } .title { position: relative; @include title; .iconfont { position: absolute; left: .1rem; width: .3rem; font-size: .24rem; color: $search-fontColor; } &__save { position: absolute; right: .18rem; font-size: .14rem; color: $content-fontcolor; } } .content { margin-top: .12rem; padding: 0 .18rem; background: $bgColor; &__item { display: flex; overflow: hidden; height: .44rem; line-height: .44rem; border-bottom: .01rem solid $content-bgColor; font-size: .14rem; &__label { color: $content-fontcolor; } &__input { flex: 1; border: none; outline: none; } } } </style>
src/views/addressSelect/AddressSelect.vue
<template> <div class="wrapper"> <div class="title">地址选择</div> <div class="empty" v-if="addressList.length === 0" > 暂无地址信息 </div> <div class="address" v-if="addressList.length > 0" > <div class="address__item" v-for="address in addressList" :key="address._id" @click="() => handleAddressClick(address._id)" > <p class="address__item__basic"> {{address.name}} <span class="address__item__phone">{{address.phone}}</span> </p> <p class="address__item__address"> {{address.city}}{{address.department}}{{address.houseNumber}} </p> </div> </div> </div> </template> <script> import { ref } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { get } from '../../utils/request'; // 地址列表获取逻辑 const useAddressListEffect = () => { const addressList = ref([]); const getAddressList = async () => { const result = await get('/api/user/address') if (result?.errno === 0 && result?.data?.length) { addressList.value = result.data; } } return { addressList, getAddressList }; } export default { name: 'AddressSelect', setup() { const router = useRouter(); const route = useRoute(); const { addressList, getAddressList } = useAddressListEffect(); getAddressList(); const handleAddressClick = (id) => { const path = route.query.path; router.push(`${path}?addressId=${id}`); }; return { addressList, handleAddressClick } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .wrapper { overflow-y: scroll; position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: $darkBgColor; } .title { position: relative; @include title; } .address { margin: .16rem .18rem 0 .18rem; &__item { position: relative; box-sizing: border-box; padding: .18rem .63rem .18rem .16rem; margin-bottom: .16rem; background: $bgColor; border-radius: .04rem; &__basic { line-height: .2rem; margin: 0; font-size: .14rem; color: $light-fontColor; } &__phone { margin-left: .66rem; } &__address { line-height: .2rem; margin: .08rem 0 0 0; font-size: .14rem; color: $content-fontcolor; } } .iconfont { transform: rotate(180deg); position: absolute; right: .16rem; top: .44rem; color: $light-fontColor; font-size: .2rem; } } .empty { @include empty; } </style>
src/views/cartList/CartList.vue
<template> <div class="wrapper"> <div class="title">我的全部购物车</div> <div class="cart" v-for="(cart, key) in list" :key="key" @click="() => handleCartClick(key)" > <div className="cart__title">{{cart.shopName}}</div> <div class="cart__item" v-for="(product, innerKey) in cart.productList" :key="innerKey"> <img class="cart__image" :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="product.imgUrl" /> <div class="cart__content"> <p class="cart__content__title">{{product.name}}</p> <p class="cart__content__price"> <span class="yen">¥</span>{{product.price}} X {{product.count}} <span class="cart__content__total"> <span class="yen">¥</span>{{(product.price * product.count).toFixed(2)}} </span> </p> </div> </div> <div class="cart__total"> 共计 {{cart.total}} 件 </div> </div> <div v-if="Object.keys(list).length === 0" class="empty" >暂无购物数据</div> </div> <Docker :currentIndex="1"/> </template> <script> import Docker from '../../components/Docker'; import { useRouter } from 'vue-router'; export default { name: 'CartList', components: { Docker }, setup() { const list = JSON.parse(localStorage.cartList || '[]'); // 计算购物车总件数的逻辑 for(let i in list) { const cart = list[i]; const productList = cart.productList; let total = 0; for(let j in productList) { const product = productList[j]; total += product['count']; } cart.total = total; } // 处理点击 const router = useRouter(); const handleCartClick = (key) => { router.push(`/orderConfirmation/${key}`); } return { list, handleCartClick } } } </script> <style lang="scss" scoped> @import '../../style/viriables.scss'; @import '../../style/mixins.scss'; .wrapper { overflow-y: auto; @include fix-content; background: $darkBgColor; } .title { @include title; } .cart { margin: .16rem; padding-bottom: .16rem; background: $bgColor; &__title { padding: .16rem; line-height: .22rem; font-size: .16rem; color: $content-fontcolor; @include ellipsis; } &__item { display: flex; padding: 0 .16rem .16rem .16rem; } &__image { margin-right: .16rem; width: .46rem; height:.46rem; } &__content { flex: 1; .yen { font-size: .12rem; } &__title { margin: 0; line-height: .2rem; font-size: .14rem; color: $content-fontcolor; @include ellipsis; } &__price { margin: 0; font-size: .14rem; color: $hightlight-fontColor; } &__total { float: right; color: $dark-fontColor; } } &__total { line-height: .28rem; margin: 0 .16rem; color: $light-fontColor; font-size: .14rem; text-align: center; background: $search-bgColor; } } .empty { @include empty; } </style>
src/views/orderConfirmation/Order.vue
<template> <div class="order"> <div class="order__price">实付金额 <b>¥{{calculations.price}}</b></div> <div v-show="showSubmitBtn" 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, addressId) => { 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, shopId, shopName: shopName.value, isCanceled, products }) if (result?.errno === 0) { const cartList = JSON.parse(localStorage.cartList || '{}'); delete cartList[shopId]; localStorage.cartList = JSON.stringify(cartList); 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, route.query.addressId) const { showConfirm, handleShowConfirmChange } = useShowMaskEffect() return { showSubmitBtn: !!route.query.addressId, 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/TopArea.vue
<template> <div class="top"> <div class="top__header"> <div class="iconfont top__header__back" @click="handleBackClick" ></div> 确认订单 </div> <div class="top__receiver" @click="handleAddressClick"> <div class="top__receiver__title">收货地址</div> <div class="top__receiver__address"> {{ hasAddress ? `${data.city}${data.department}${data.houseNumber}` : '请选择收货地址' }} </div> <div v-if="hasAddress" class="top__receiver__info"> <span class="top__receiver__info__name">{{data.name}}</span> <span class="top__receiver__info__name">{{data.phone}}</span> </div> <div class="iconfont top__receiver__icon"></div> </div> </div> </template> <script> import { reactive } from 'vue'; import { onBeforeMount } from '@vue/runtime-core'; import { useRouter, useRoute } from 'vue-router'; import { get } from '../../utils/request'; export default { name: 'TopArea', setup() { const router = useRouter(); const route = useRoute(); const data = reactive({}); const addressId = route.query.addressId; const handleBackClick = () => { router.back() } const handleAddressClick = () => { router.push(`/addressSelect?path=${route.path}`) } onBeforeMount(async() => { if(addressId) { const result = await get(`/api/user/address/${addressId}`); if (result?.errno === 0) { const resultData = result.data; data.city = resultData.city; data.department = resultData.department; data.houseNumber = resultData.houseNumber; data.name = resultData.name; data.phone = resultData.phone; } } }); return { data, hasAddress: !!addressId, handleBackClick, handleAddressClick, } } } </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: .2rem; 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/style/viriavles.scss
$dark-fontColor: #000; $content-fontcolor: #333; $medium-fontColor: #666; $light-fontColor: #999; $content-notice-fontcolor: #777; $content-bgColor: #F1F1F1; $search-bgColor: #F5F5F5; $search-fontColor: #B7B7B7; $hightlight-fontColor: #E93B3B; $btn-bgColor: #0091FF; $bgColor: #FFF; $darkBgColor: #F8F8F8;