ทำไมเว็บ EC ควรแยก 'เพิ่มลงตะกร้า' กับ 'ซื้อเลย' และวิธี Implement

Vue3 UX TypeScript Frontend

บทนำ

เวลาสร้างเว็บ EC เคยลังเลไหมว่าปุ่มซื้อในหน้าสินค้าควร
เพิ่มลงตะกร้า หรือควรพาไปซื้อโดยตรง

นี่คือแนวคิดที่ควรแยกให้ชัดเจนในระดับการออกแบบ UX
ถ้าปนกัน user จะสับสนว่า “เอ๊ะ ซื้อไปแล้วเหรอ หรือแค่ใส่ตะกร้าไว้”


ปัญหาที่สองปุ่มนี้แก้ไข

พฤติกรรมการซื้อสองแบบของ User

พฤติกรรม A: อยากซื้อรวมกันหลายชิ้น

“หนังเรื่องนี้กับเรื่องนั้น แล้วก็อีกเรื่อง อยากเช่ารวมกันสามเรื่อง”

→ เพิ่มลงตะกร้า → ตรวจสอบที่หน้าตะกร้า → ชำระเงินรวมกันทีเดียว

พฤติกรรม B: อยากซื้อแค่อันนี้ตอนนี้เลย

“ชอบหนังเรื่องนี้ อยากดูเลยทันที”

→ จากหน้าสินค้าไปหน้าชำระเงินโดยตรง → เสร็จทันที

ถ้าพยายามจัดการทั้งสองแบบด้วยปุ่มเดียว UX ของแบบใดแบบหนึ่งจะถูกเสียสละไป


ความแตกต่างของบทบาทปุ่ม

ปุ่มบทบาทปลายทางที่เปลี่ยนหน้าไป
เพิ่มลงตะกร้าเพิ่มสินค้าลงตะกร้าCart drawer หรือหน้าตะกร้า
ซื้อเลยเข้าสู่ checkout flow โดยตรงหน้าชำระเงิน

ตัวอย่าง Implementation ใน Vue 3

<!-- FilmDetailModal.vue -->
<template>
  <div class="film-actions">
    <!-- เพิ่มลงตะกร้า: เพิ่มลงตะกร้าและเปิด drawer -->
    <button
      class="btn btn-outline-primary"
      @click="addToCart"
      :disabled="isInCart"
    >
      <i class="bi bi-cart-plus" />
      {{ isInCart ? 'เพิ่มลงตะกร้าแล้ว' : 'เพิ่มลงตะกร้า' }}
    </button>

    <!-- ซื้อเลย: ไปหน้าชำระเงินโดยตรง -->
    <button
      class="btn btn-primary"
      @click="buyNow"
    >
      <i class="bi bi-lightning-fill" />
      ซื้อเลย
    </button>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useCartStore } from '@/stores/cartStore'
import { useRouter } from 'vue-router'

interface Props {
  filmId: number
  title: string
  rentalRate: number
}
const props = defineProps<Props>()
const cartStore = useCartStore()
const router = useRouter()

const isInCart = computed(() =>
  cartStore.items.some(item => item.filmId === props.filmId)
)

// เพิ่มลงตะกร้า
function addToCart() {
  cartStore.addItem({
    filmId: props.filmId,
    title: props.title,
    rentalRate: props.rentalRate,
  })
  cartStore.openDrawer()  // เปิด side drawer เพื่อแสดงตะกร้า
}

// ซื้อเลย: ไปหน้าชำระเงินเฉพาะรายการนี้เท่านั้น
function buyNow() {
  router.push({
    path: '/checkout',
    query: {
      mode: 'buy-now',
      filmId: props.filmId,
    },
  })
}
</script>

การจัดการ mode “ซื้อเลย” ที่หน้าชำระเงิน

เมื่อถูกเข้าถึงด้วย /checkout?mode=buy-now&filmId=123
เป้าหมายการชำระเงินจะเป็นสินค้าที่ระบุเท่านั้น ไม่ใช่เนื้อหาตะกร้า

// CheckoutView.vue
import { useRoute } from 'vue-router'
import { useCartStore } from '@/stores/cartStore'
import { ref, onMounted } from 'vue'

const route = useRoute()
const cartStore = useCartStore()

// รายการที่จะชำระเงิน
const checkoutItems = ref([])

onMounted(async () => {
  if (route.query.mode === 'buy-now' && route.query.filmId) {
    // ซื้อเลย: เฉพาะรายการนี้
    const film = await fetchFilm(Number(route.query.filmId))
    checkoutItems.value = [film]
  } else {
    // flow ปกติ: เนื้อหาตะกร้า
    checkoutItems.value = cartStore.items
  }
})

Cart Drawer UI

เมื่อกด “เพิ่มลงตะกร้า” การเปิด drawer จากด้านข้างโดยไม่เปลี่ยนหน้าคือ UX ที่ดี
ทำให้ user เลือกได้ว่าจะ “ตรวจสอบตะกร้า → ซื้อของต่อ” หรือ “ตรวจสอบตะกร้า → ไปชำระเงิน”

<!-- CartDrawer.vue -->
<template>
  <Transition name="slide-right">
    <div v-if="cartStore.isDrawerOpen" class="cart-drawer">
      <div class="cart-drawer-header">
        <h5>ตะกร้า ({{ cartStore.totalItems }} รายการ)</h5>
        <button @click="cartStore.closeDrawer()">✕</button>
      </div>
      
      <div class="cart-drawer-body">
        <CartItem
          v-for="item in cartStore.items"
          :key="item.filmId"
          :item="item"
        />
      </div>

      <div class="cart-drawer-footer">
        <div class="total">รวม: ¥{{ cartStore.totalAmount }}</div>
        <RouterLink to="/checkout" @click="cartStore.closeDrawer()">
          <button class="btn btn-primary w-100">
            ไปที่ Checkout
          </button>
        </RouterLink>
      </div>
    </div>
  </Transition>
  
  <!-- overlay -->
  <div
    v-if="cartStore.isDrawerOpen"
    class="cart-overlay"
    @click="cartStore.closeDrawer()"
  />
</template>

ข้อผิดพลาดที่พบบ่อย

❌ กด “เพิ่มลงตะกร้า” แล้วเปลี่ยนไปหน้าชำระเงินทันที

กลายเป็นการออกแบบที่ไม่มีแนวคิดเรื่องตะกร้าอยู่จริง
user ที่อยากซื้อหลายชิ้นรวมกันจะเดือดร้อน

❌ “ซื้อเลย” เพิ่มลงตะกร้าก่อนแล้วค่อยไปหน้าชำระเงิน

ตะกร้าจะปนเปื้อน
เมื่อเปิดตะกร้าหลังกด “ซื้อเลย” สินค้าที่ซื้อไปแล้วจะยังค้างอยู่

❌ ปุ่มเดียวที่สลับระหว่าง “เพิ่มลงตะกร้า” กับ “ซื้อ”

เพิ่มขั้นตอนให้ user ต้องเลือกความตั้งใจของตัวเอง การวางสองปุ่มแยกกันเข้าใจง่ายกว่า


สรุป

เพิ่มลงตะกร้าซื้อเลย
จุดประสงค์เตรียมซื้อหลายสินค้ารวมกันซื้อสินค้าชิ้นเดียวทันที
ปลายทางCart drawer / หน้าตะกร้าหน้าชำระเงิน
ผลต่อตะกร้าเพิ่มลงตะกร้าไม่แตะตะกร้า
พฤติกรรมหลังจากนั้นหาสินค้าต่อได้จดจ่ออยู่กับ flow ชำระเงินจนเสร็จ

การที่ Amazon และ Rakuten แยก “เพิ่มลงตะกร้า” กับ “ซื้อเลย” ก็มาจากเหตุผลการออกแบบ UX แบบนี้เอง


การใช้งานจริงในแอพนี้

แอพเช่า DVD นี้ไม่ได้ใช้ Vue Router หรือ Pinia แต่ใช้ emit event กับ currentPage ref ในการทำสิ่งนี้

ปุ่ม CTA ในหน้าละเอียดภาพยนตร์ (โค้ดจริง)

<!-- FilmDetailView.vue -->
<div class="cta-row">
  <!-- คำที่แสดงจะเปลี่ยนตามว่าเป็น section เช่าหรือไม่ -->
  <template v-if="isRentalSection">
    <button class="btn-primary" @click="emit('direct-checkout', film)">เช่าเดี๋ยวนี้ (ชำระเงิน)</button>
    <button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">เพิ่มลงตะกร้า</button>
  </template>
  <template v-else>
    <button class="btn-primary" @click="emit('direct-checkout', film)">ซื้อเลย (ชำระเงิน)</button>
    <button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">เพิ่มลงตะกร้า</button>
  </template>

  <button class="btn-secondary" @click="emit('add-watch-later', film)">เพิ่มลงรายการโปรด</button>
</div>

การกำหนด type ของ emit:

const emit = defineEmits<{
  (e: 'back'): void
  (e: 'direct-checkout', film: PublicFilmSummary): void
  (e: 'add-to-cart', film: PublicFilmSummary): void
  (e: 'add-watch-later', film: PublicFilmSummary): void
}>()

การจัดการใน App.vue (โค้ดจริง)

// App.vue (ฝั่งรับ emit)

// เพิ่มลงตะกร้า: บันทึกลง localStorage ผ่าน useCart composable
function handleAddToCart(film: PublicFilmSummary) {
  const added = addToCart(film)
  if (added) {
    cartToastMessage.value = `เพิ่ม "${film.title}" ลงตะกร้าแล้ว`
    cartToastTimer = setTimeout(() => { cartToastMessage.value = '' }, 2000)
  }
}

// ซื้อเลย: สลับ currentPage เพื่อไปหน้าชำระเงิน
const handleDirectCheckout = (film: PublicFilmSummary) => {
  selectedFilm.value = film
  currentPage.value = 'checkout'
}

ตะกร้าไม่ใช้ router หรือ global store แต่ useCart composable ทำหน้าที่เป็น singleton จัดการบันทึกถาวรลง localStorage การเปลี่ยนหน้าก็เพียงแค่เขียนทับ currentPage ref ชนิด union literal ก็เสร็จสมบูรณ์

หน้าจอจริง

ปุ่ม CTA ในหน้าละเอียดภาพยนตร์ (“เช่าเดี๋ยวนี้” กับ “เพิ่มลงตะกร้า”):

ปุ่ม CTA ในหน้าละเอียดภาพยนตร์

Cart drawer หลังกด “เพิ่มลงตะกร้า”:

Cart drawer

หน้าชำระเงินที่เปลี่ยนไปด้วย “เช่าเดี๋ยวนี้”:

หน้าชำระเงิน


แผนที่บทความของ Series นี้

สร้างแอป DVD Rental สำหรับผู้ใช้ปลายทาง — โครงสร้าง Vue 3 + Spring Boot คู่กับแอปผู้ดูแลระบบ พร้อมแผนที่บทความ

ส่งข้อความได้ตามสบาย

ไม่ว่าจะเป็นการว่าจ้างงาน แนะนำโปรเจกต์ ความคิดเห็น หรือคำถาม ยินดีรับทั้งหมด ฉันหวังเป็นอย่างยิ่งว่าจะได้เชื่อมต่อกับผู้ที่มีอุดมการณ์อันสูงส่งเช่นเดียวกัน ฉันจะมุ่งมั่นท้าทายในสิ่งที่ทุ่มเททั้งชีวิตต่อไป ขอขอบคุณและฝากเนื้อฝากตัวด้วยครับ/ค่ะ