为了账号安全,请及时绑定邮箱和手机立即绑定

【学习打卡】第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">&#xe6f2;</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">
        &#xe6f2;
      </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">&yen;</span>{{product.price}} X {{product.count}}
            <span class="cart__content__total">
              <span class="yen">&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"
        >&#xe6f2;</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">&#xe6f2;</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 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消