iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0

Aha~終於來到首頁了,這邊規劃很簡單,一個輪播的廣告版位、及每個產品分類的前四個商品hightlights,用lazy loading來處理這邊,往下滑才去要資料。
landing page mockup

目錄

  • 廣告:Slider
  • Category highlights
  • 閒聊碎碎念

廣告:Carousel

Category highlights

這邊沒有什麼大數據,單純把前4筆拉出來而已...
這邊規劃是:

  1. 打API:把category的名字拉出來當作是key,再透過key拉出每個category前4筆資料
  2. 何時拉資料?當畫面滾到最底,而且還有資料時。
  3. UX優化:正在loading資料時需要一個isLoading的flag,讓user辨別。
  4. UX優化:為了避免資料還沒load出來,又送出下一筆請求,又或是請求太頻繁,所以會使用loadash.throttle來把關。

安裝lodash.throttle

npm i --save lodash.throttle 
npm i --save-dev @types/lodash.throttle
// ./pages/index.vue
<script lang="ts" setup>
import type { ProductListModel, ProductDetailModel, CategoryNameListModel } from '~/models/apiModel';
import type { landingPageProdGroupModel } from '~/models/viewModel';
import throttle from 'lodash.throttle';

const cateStore = useCategory();
const {
  cateNameList,
} = storeToRefs(cateStore);
const {
  getCateNameList
} = cateStore;

// carousel
const currentIndex: Ref<number> = ref(0);
const images = ref([
  'https://dummyjson.com/image/800x400/008080/ffffff?text=Hello+World+NO+1',
  'https://dummyjson.com/image/800x400/ff9b9b/ffffff?text=Hello+Athem+NO+2',
  'https://dummyjson.com/image/800x400/ffe1b2/ffffff?text=Hello+World+NO+3',
  'https://dummyjson.com/image/800x400/95e5da/ffffff?text=Hello+Athem+NO+4',
  'https://dummyjson.com/image/800x400/bfc2ff/ffffff?text=Hello+World+NO+5',
]);

// product's highlight
const cateList: Ref<CategoryNameListModel> = ref([]);
const productGroupList: Ref<Array<landingPageProdGroupModel>> = ref([]);
const isLoading: Ref<boolean> = ref(false);
const idx: Ref<number> = ref(0);
const isEnd: Ref<boolean> = ref(false);
const container: Ref<HTMLElement | null> = ref(null);



// ======= carousel =======
let intervalId = null;

const carouselStyle = computed(() => {
  return {
    transform: `translateX(-${currentIndex.value * 100}%)`,
    transition: 'transform 1s ease'
  };
});

const nextSlide = () => {
  currentIndex.value = (currentIndex.value + 1) % images.value.length;
};

const startCarousel = () => {
  intervalId = setInterval(nextSlide, 3000);
};

const stopCarousel = () => {
  if (intervalId) {
    clearInterval(intervalId);
  }
};


// ====== product ======
const loadMoreData = async () => {
  if (isLoading.value || isEnd.value) return;
  isLoading.value = true;
  const cateVal = cateList.value;

  try {
    const data: ProductListModel = await $fetch(`https://dummyjson.com/products/category/${cateVal[idx.value]}?limit=4`, { responseType: 'json' });
    const products = data.products;
    if (idx.value >= cateVal.length) {
      isEnd.value = true;
    } else {
      productGroupList.value = [...productGroupList.value, {categoryName: products[0].category, productList: products}]
      idx.value += 1;
    }
  } catch (err) {
    console.error('Failed to load data:', err);
  } finally {
    isLoading.value = false;
  }
};

let throt_fun = throttle(async () => {
  await loadMoreData();
}, 1000);

const handleScrollAction = async () => {
  const scrollTop = window.scrollY || window.pageYOffset;
  const scrollHeight = document.documentElement.scrollHeight;
  const clientHeight = window.innerHeight;

  // determinate is user scrolls to the buttom
  if (scrollTop + clientHeight >= scrollHeight - 10) {
    throt_fun();
  }
};

onMounted(async() => {
  startCarousel();
  await getCateNameList().then(() => {
    cateList.value = cateNameList.value;
  }).then(() => {
    loadMoreData();
  }).catch( err => {console.error(err)})
  
  nextTick(() => {
      window.addEventListener('scroll', handleScrollAction);
  });
});

onUnmounted(() => {
  stopCarousel();

  if (container.value) {
    window.removeEventListener('scroll', handleScrollAction);
  }
});
</script>

<template>
  <div class="container" ref="container">
    <div>
      <div class="carousel">
        <div class="carousel-wrapper">
          <div class="carousel-images" :style="carouselStyle">
            <img v-for="(image, index) in images" :key="index" :src="image" class="carousel-image" />
          </div>
        </div>
      </div>
    </div>
    
    <div v-for="list in productGroupList">
      <div>
        <h2>{{ list.categoryName }}</h2>
        <div class="flex">
          <div v-for="prod in list.productList" :key="prod.id">
            <p>{{ prod.title }}</p>
            <img :src="prod.thumbnail" alt="">
            <p>{{ prod.price }} cad</p>
            <p>{{ prod.stock }} left</p>
            <p>rating: {{ prod.rating }}</p>
            <button>add to cart</button>
          </div>
        </div>
      </div>
    </div>
    <div v-if="isLoading">
      <h1>Loading...</h1> 
    </div>
  </div>
</template>

<style scoped>
.flex {
  display: flex;
}

.container {
  height: 100%;
}

.carousel {
  overflow: hidden;
  width: 800px;
  height: 400px; 
  position: relative;
}

.carousel-wrapper {
  display: flex;
}

.carousel-images {
  display: flex;
  width: 100%;
  height: 100%;
}

.carousel-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

閒聊碎碎念

問你喲~(講得好像有人會回我)像Carousel之類的元件,你通常會直接使用外掛?還是自己手刻呢?我沒有一個答案,好像如果功能很簡單,手刻沒什麼不好,但如果很龐大那種操作,就用外掛吧。今天的滾動拉資料,因為過去都做後台還真沒機會做到這樣的動作,這就是做side project的意義吧,學習工作外想envolve的部分。


上一篇
[Day 15] 來做部分共用的components吧+設定ICON!!
下一篇
[Day 18] Product List Page + Router
系列文
NUXT3xVUE3xPINIA: 從零開始寫電商29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言