手记

【学习打卡】第13天 【2022版】Vue3 系统入门与项目实战第十四讲

课程名称: 2022持续升级 Vue3 从入门到实战 掌握完整知识体系

课程章节: 【2022加餐】商品搜索 & 购物车功能实现

主讲老师: Dell

课程内容:

今天学习的内容包括:
如何进行购物车的优化及mixin的复用

课程收获:

src/views/search/Search.vue

<template>
  <div class="wrapper">
    <div class="search">
      <span class="iconfont">&#xe62d;</span>
      <input
        class="search__area"
        @change="handleSearchChange"
        placeholder="山姆会员商店优惠商品"
      />
      <div class="search__cancel" @click="handleCancelSearchClick">取消</div>
    </div>
    <div class="area" v-if="history.length">
      <h4 class="area__title">
        搜索历史
        <span
          class="area__title__clear"
          @click="handleClearHistoryClick"
        >清除搜索历史</span>
      </h4>
      <ul class="area__list">
        <li
          class="area__list__item"
          v-for="item in history"
          :key="item"
          @click="() => goToSearchList(item)"
        >{{item}}</li>
      </ul>
    </div>
    <div class="area">
      <h4 class="area__title">热门搜索</h4>
      <ul class="area__list">
        <li
          class="area__list__item"
          v-for="item in hotWordList"
          :key="item"
          @click="() => goToSearchList(item)"
        >{{item}}</li>
      </ul>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { get } from '../../utils/request';

// 热词相关逻辑
const useHotWordListEffect = () => {
  const hotWordList = ref([]);
  const getHotWorList = async () => {
    const result = await get('/api/shop/search/hot-words')
    if (result?.errno === 0 && result?.data?.length) {
      hotWordList.value = result.data
    }
  }
  return { hotWordList, getHotWorList };
}

export default {
  name: 'Search',
  setup() {
    const router = useRouter();
    const history = ref(JSON.parse(localStorage.history || '[]'));
    // 当用户输入搜索内容后,执行的操作
    const handleSearchChange = (e) => {
      const searchValue = e.target.value;
      if(!searchValue) return;
      const hasValue = history.value.find(item => item === searchValue);
      if(!hasValue) {
        history.value.push(searchValue);
        localStorage.history = JSON.stringify(history.value);
      }
      router.push(`/searchList?keyword=${searchValue}`);
    }
    // 当清理历史记录时,执行的操作
    const handleClearHistoryClick = () => {
      history.value = [];
      localStorage.history = JSON.stringify([]);
    }
    // 当取消搜索时,执行的操作
    const handleCancelSearchClick = () => {
      router.back();
    }
    // 页面跳转逻辑
    const goToSearchList = (keyword) => {
      router.push(`/searchList?keyword=${keyword}`);
    }
    // 使用热词逻辑
    const { hotWordList, getHotWorList } = useHotWordListEffect();
    getHotWorList();
    
    return {
      history,
      hotWordList,
      goToSearchList, 
      handleSearchChange,
      handleClearHistoryClick,
      handleCancelSearchClick
    };
  }
}
</script>

<style lang="scss" scoped>
@import '../../style/viriables.scss';
.wrapper {
  margin: 0 .18rem;
  .search {
    position: relative;
    display: flex;
    line-height: .32rem;
    margin-top: .16rem;
    color: $content-fontcolor;
    .iconfont {
      position: absolute;
      left: .16rem;
      color: $search-fontColor;
    }
    &__area {
      flex: 1;
      padding: 0 .12rem 0 .44rem;
      background: $search-bgColor;
      border-radius: .16rem;
      border: none;
      outline: none;
      font-size: .14rem;
    }
    &__cancel {
      margin-left: .12rem;
      font-size: .16rem;
    }
  }
  .area {
    margin-top: .24rem;
    &__title {
      line-height: .22rem;
      margin: 0;
      font-size: .16rem;
      font-weight: normal;
      color: $content-fontcolor;
      &__clear {
        float: right;
        font-size: .14rem;
      }
    }
    &__list {
      margin: 0 0 0 -.1rem;
      padding: 0;
      list-style-type: none;
      &__item {
        line-height: .32rem;
        margin-left: .1rem;
        margin-top: .12rem;
        padding: 0 .1rem;
        font-size: .14rem;
        background: $search-bgColor;
        display: inline-block;
        border-radius: .02rem;
        color: $medium-fontColor;
      }
    }
  }
}
</style>

src/views/home/Nearby.vue

<template>
  <div class="nearby">
    <h3 class="nearby__title">附近店铺</h3>
    <router-link
      v-for="item in nearbyList"
      :key="item._id"
      :to="`/shop/${item._id}`"
    >
      <ShopInfo :item="item" />
    </router-link>
  </div>
</template>

<script>
import { ref } from 'vue';
import { get } from '../../utils/request';
import ShopInfo from '../../components/ShopInfo';

const useNearbyListEffect = () => {
  const nearbyList = ref([]);
  const getNearbyList = async () => {
    const result = await get('/api/shop/hot-list')
    if (result?.errno === 0 && result?.data?.length) {
      nearbyList.value = result.data
    }
  }
  return { nearbyList, getNearbyList}
}

export default {
  name: 'Nearby',
  components: { ShopInfo },
  setup() {
    const { nearbyList, getNearbyList } = useNearbyListEffect();
    getNearbyList();
    return { nearbyList };
  }
}
</script>

<style lang="scss" scoped>
@import '../../style/viriables.scss';
.nearby {
  &__title {
    margin: .16rem 0 .02rem 0;
    font-size: .18rem;
    font-weight: normal;
    color: $content-fontcolor;
  }
  a {
    text-decoration: none;
  }
}
</style>

src/views/searchList/searchList.vue

<template>
  <div class="wrapper">
    <div class="search">
      <div class="search__back iconfont" @click="handleBackClick">
        &#xe6f2;
      </div>
      <div class="search__content">
        <span class="search__content__icon iconfont">&#xe62d;</span>
        <input
          class="search__content__input"
          placeholder="请输入商品名称"
          v-model="keyword"
          @change="handleSearchInputChange"
        />
      </div>
    </div>
    <router-link
      v-for="item in searchList"
      :key="item._id"
      :to="`/shop/${item._id}`"
    >
      <ShopInfo :item="item" />
    </router-link>
  </div>
</template>

<script>
import { ref } from 'vue'; 
import { useRouter, useRoute } from 'vue-router';
import { get } from '../../utils/request';
import ShopInfo from '../../components/ShopInfo';

// 点击回退逻辑
const useBackRouterEffect = () => {
  const router = useRouter()
  const handleBackClick = () => {
    router.back()
  }
  return handleBackClick
}

// 热词相关逻辑
const useSearchListEffect = () => {
  const searchList = ref([]);
  const getSearchList = async (keyword) => {
    const result = await get('/api/shop/search', { keyword });
    if (result?.errno === 0 && result?.data?.length) {
      searchList.value = result.data
    }
  }
  return { searchList, getSearchList };
}

export default {
  name: 'SearchList',
  components: { ShopInfo },
  setup() {
    const route = useRoute();
    // 搜索关键词逻辑
    const keyword = ref(route.query.keyword || '');
    const handleSearchInputChange = () => {
      getSearchList(keyword.value)
    }
    const handleBackClick = useBackRouterEffect();
    // 获取搜索列表
    const { searchList, getSearchList } = useSearchListEffect();
    getSearchList(keyword.value);

    return {
      keyword,
      searchList,
      handleBackClick,
      handleSearchInputChange
    }
  }
}
</script>

<style lang="scss" scoped>
@import '../../style/viriables.scss';
.wrapper {
  padding: 0 .18rem;
  a {
    text-decoration: none;
  }
}
.search {
  display: flex;
  margin: .14rem 0 .04rem 0;
  line-height: .32rem;
  &__back {
    width: .3rem;
    font-size: .24rem;
    color: #B6B6B6;
  }
  &__content {
    display: flex;
    flex: 1;
    background: $search-bgColor;
    border-radius: .16rem;
    &__icon {
      width: .44rem;
      text-align: center;
      color: $search-fontColor;
    }
    &__input {
      display: block;
      width: 100%;
      padding-right: .2rem;
      border: none;
      outline: none;
      background: none;
      height: .32rem;
      font-size: .14rem;
      color: $content-fontcolor;
      &::placeholder {
        color: $content-fontcolor;
      }
    }
  }
}
</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" :src="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 {
  margin-top: .5rem;
  line-height: .5rem;
  text-align: center;
  font-size: .16rem;
  color: $light-fontColor;
}
</style>

src/components/Shopinfo.vue

<template>
  <div class="shop">
    <img :src="item.imgUrl" class="shop__img">
    <div
      :class="{'shop__content': true, 'shop__content--bordered': hideBorder ? false: true}"
    >
      <div class="shop__content__title">{{item.name}}</div>
      <div class="shop__content__tags">
        <span class="shop__content__tag">月售: {{item.sales}}</span>
        <span class="shop__content__tag">起送: {{item.expressLimit}}</span>
        <span class="shop__content__tag">基础运费: {{item.expressPrice}}</span>
      </div>
      <p class="shop__content__highlight">{{item.slogan}}</p>
      <div v-if="item.products" class="shop__products">
        <div v-for="product in item.products" :key="product.name" class="shop__product">
          <img :src="product.imgUrl" class="shop__product__img" />
          <p class="shop__product__title">{{product.name}}</p>
          <p class="shop__product__price">
            <span class="yen">&yen;</span><span class="price">{{product.price}}</span>
            <span class="origin">&yen;{{product.price}}</span>
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ShopInfo',
  props: ['item', 'hideBorder']
}
</script>

<style lang="scss" scoped>
@import '../style/viriables.scss';
@import '../style/mixins.scss';
.shop {
  display: flex;
  padding-top: .12rem;
  &__img {
    margin-right: .16rem;
    width: .56rem;
    height: .56rem;
  }
  &__content {
    flex: 1;
    padding-bottom: .12rem;
    &--bordered {
      border-bottom: .01rem solid $content-bgColor;
    }
    &__title {
      line-height: .22rem;
      font-size: .16rem;
      color: $content-fontcolor;
    }
    &__tags {
      margin-top: .08rem;
      line-height: .18rem;
      font-size: .13rem;
      color: $content-fontcolor;
    }
    &__tag {
      margin-right: .16rem;
    }
    &__highlight {
      margin: .08rem 0 0 0;
      line-height: .18rem;
      font-size: .13rem;
      color: $hightlight-fontColor;
    }
  }
  &__products {
    overflow: hidden;
    margin: .08rem .07rem 0 -.18rem;
  }
  &__product {
    width: 33.33%;
    padding-left: .18rem;
    box-sizing: border-box;
    float: left;
    &__img {
      width: 100%;
    }
    &__title {
      margin: .04rem 0 0 0;
      line-height: .17rem;
      font-size: .12rem;
      color: $content-fontcolor;
      @include ellipsis;
    }
    &__price {
      line-height: .2rem;
      margin: .02rem 0 0 0;
      color: $light-fontColor;
      font-size: .14rem;
      @include ellipsis;
      .yen {
        font-size: .12rem;
        color: $hightlight-fontColor;
      }
      .price {
        color: $hightlight-fontColor;
      }
      .origin {
        margin-left: .06rem;
        font-size: .12rem;
        text-decoration: line-through;
      }
    }
  }
}
</style>

src/style/minixs.scss

@mixin ellipsis {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

@mixin fix-content {
  position: absolute;
  left: 0;
  top: 0;
  bottom: .5rem;
  right: 0;
}

@mixin title {
  line-height: .44rem;
  background: $bgColor;
  font-size: .16rem;
  color: $content-fontcolor;
  text-align: center;
}

0人推荐
随时随地看视频
慕课网APP