商品詳情
切換照片,選擇顏色和數量後加入購物車。可以選擇的範圍會依庫存數量而改變。
商品詳細資訊
重點
- 1
讓目前選擇的東西一眼就看得出來
正在看的照片加上外框,選擇的顏色顯示成黑色按鈕。選了哪一個一目了然。
- 2
有理由不能按的按鈕,就讓它無法點擊
數量不能少於 1,也不能超過庫存。沒有庫存的顏色不能選。比起按下去之後才顯示錯誤,一開始就無法點擊更容易理解。
- 3
加入購物車後,用數量和通知告訴使用者
上方購物車的數量會增加,右下角會顯示「已將 N 件商品加入購物車」。畫面不會切換的操作,要把結果顯示出來。
使用的工具
點擊查看第 2 部分的比較
修改程式碼
修改程式碼,右邊的預覽會跟著變化。
import { useState } from "react" import { MinusIcon, PlusIcon, ShoppingCartIcon } from "lucide-react" import { toast } from "sonner" import { Button } from "./components/ui/button" import { Toaster } from "./components/ui/sonner" // 商品資料(實際專案中從伺服器取得) const product = { name: "無線耳機", category: "3C 家電", price: 2550, rating: 4.5, reviews: 128, description: "主動降噪,讓你專心享受音樂。充電一次可連續使用 8 小時。", // 用顏色代替商品照片(4 張) images: ["#0ea5e9", "#0284c7", "#7dd3fc", "#e0f2fe"], // 每種顏色的庫存 colors: [ { name: "黑色", stock: 5 }, { name: "白色", stock: 2 }, { name: "藍色", stock: 0 }, ], } export default function App() { // ① 畫面要記住的值(state) //const imageIndex = 0 // 大圖的編號(0~3) const [imageIndex, setImageIndex] = useState(0) const [color, setColor] = useState("黑色") // 目前選取的顏色 const [quantity, setQuantity] = useState(1) // 數量 const [cartCount, setCartCount] = useState(0) // 購物車裡的件數 const [colors, setColors] = useState(product.colors) // 每種顏色的庫存(加入購物車後減少) // 目前所選顏色的庫存數 const stock = colors.find((c) => c.name === color)?.stock ?? 0 // 點擊「加入購物車」時 function addToCart() { // 購物車件數增加所選的數量 setCartCount(cartCount + quantity) setColors(colors.map((c) => (c.name === color ? { ...c, stock: c.stock - quantity } : c))) setQuantity(1) toast.success(`已將 ${quantity} 件 ${product.name}(${color})加入購物車`) } return ( <div className="mx-auto my-6 w-full max-w-3xl rounded-xl border bg-card text-card-foreground"> {/* 頂端列:商店名稱與購物車件數 */} <header className="flex items-center justify-between border-b px-6 py-3 text-sm"> <span className="font-semibold">商店</span> {/* 右側:購物車件數與「清空購物車」 */} <div className="flex items-center gap-2"> <span className="flex items-center gap-1.5 rounded-md bg-muted px-2.5 py-1"> <ShoppingCartIcon className="size-4" /> 購物車 <b>{cartCount}</b> </span> <Button type="button" size="sm" variant="ghost" disabled={cartCount === 0} onClick={() => { setCartCount(0); setColors(product.colors) }}> 清空購物車 </Button> </div> </header> <div className="grid gap-8 p-6 md:grid-cols-2"> {/* 照片:1 張大圖與 4 張小圖 */} <div> <div className="aspect-square rounded-lg" style={{ backgroundColor: product.images[imageIndex] }} /> <div className="mt-3 grid grid-cols-4 gap-2"> {product.images.map((image, index) => ( <button key={image} type="button" className={imageIndex === index ? "aspect-square rounded-md ring-2 ring-foreground ring-offset-2" : "aspect-square rounded-md"} style={{ backgroundColor: image }} onClick={() => setImageIndex(index)} /> ))} </div> </div> {/* 商品資訊 */} <div> <p className="text-xs text-muted-foreground">{product.category}</p> <p className="mt-1 text-2xl font-semibold">{product.name}</p> <p className="mt-1 text-sm text-amber-500"> ★ {product.rating} <span className="text-muted-foreground">({product.reviews} 則評價)</span> </p> <p className="mt-4 text-2xl font-semibold">NT${product.price.toLocaleString()}</p> <p className="mt-4 text-sm leading-relaxed text-muted-foreground">{product.description}</p> {/* 顏色 */} <p className="mt-6 text-sm font-medium">顏色</p> <div className="mt-2 flex flex-wrap gap-2"> {colors.map((option) => ( <Button key={option.name} type="button" size="sm" variant={color === option.name ? "default" : "outline"} disabled={option.stock === 0} /* 點擊時選取此顏色;選取的顏色會反白;沒有庫存的顏色無法點擊 */ onClick={() => { setColor(option.name) setQuantity(1) }} > {option.name} </Button> ))} </div> {/* 數量 */} <p className="mt-5 text-sm font-medium">數量</p> <div className="mt-2 flex items-center gap-3"> <div className="inline-flex items-center rounded-md border"> <Button type="button" size="icon" variant="ghost" aria-label="減少一件" disabled={quantity <= 1} onClick={() => setQuantity(quantity - 1)} > <MinusIcon /> </Button> <span className="w-10 text-center text-sm">{quantity}</span> <Button type="button" size="icon" variant="ghost" aria-label="增加一件" disabled={quantity >= stock} onClick={() => setQuantity(quantity + 1)} > <PlusIcon /> </Button> </div> <span className="text-xs text-muted-foreground">庫存 {stock} 件</span> </div> <Button type="button" className="mt-6 w-full" disabled={stock === 0} onClick={addToCart}> 加入購物車 </Button> </div> </div> <Toaster position="bottom-right" /> </div> ) }