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