商品列表
用關鍵字搜尋商品、依分類篩選、排序,並以每頁 6 件分頁顯示。
商品列表
重點
- 1
不改動原始資料,只產生要顯示的部分
從全部商品開始,依序經過 filter(篩選)→ sort(排序)→ slice(分頁),產生要顯示在畫面上的部分。原本的列表維持不變。
- 2
條件一改變,就回到第 1 頁
在看第 2 頁時搜尋,結果可能只有 1 頁,畫面就會變成空的。只要條件改變,就把頁碼改回 1。
- 3
準備好「找不到結果」時的畫面
0 件時顯示「沒有符合條件的商品」,並提供清除條件的按鈕。什麼都不顯示的話,看起來就像壞掉了。
使用的工具
點擊查看第 2 部分的比較
修改程式碼
修改程式碼,右邊的預覽會跟著變化。
import { useState } from "react" import { SearchIcon } from "lucide-react" import { Button } from "./components/ui/button" import { Input } from "./components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select" type Product = { id: number name: string category: string price: number rating: number color: string // 用顏色代替商品照片 } // 商品資料(12 件) const products: Product[] = [ { id: 1, name: "無線耳機", category: "3C 家電", price: 2550, rating: 4.5, color: "#0ea5e9" }, { id: 2, name: "不鏽鋼保溫瓶", category: "廚房", price: 600, rating: 4.2, color: "#10b981" }, { id: 3, name: "後背包 20L", category: "服飾", price: 1500, rating: 4.7, color: "#f59e0b" }, { id: 4, name: "檯燈", category: "3C 家電", price: 1200, rating: 4.0, color: "#8b5cf6" }, { id: 5, name: "咖啡磨豆機", category: "廚房", price: 900, rating: 4.6, color: "#ef4444" }, { id: 6, name: "運動鞋", category: "服飾", price: 1950, rating: 4.3, color: "#ec4899" }, { id: 7, name: "行動電源", category: "3C 家電", price: 780, rating: 4.4, color: "#14b8a6" }, { id: 8, name: "平底鍋 26cm", category: "廚房", price: 690, rating: 4.1, color: "#f97316" }, { id: 9, name: "棒球帽", category: "服飾", price: 480, rating: 3.9, color: "#6366f1" }, { id: 10, name: "電熱水壺", category: "3C 家電", price: 990, rating: 4.5, color: "#84cc16" }, { id: 11, name: "砧板", category: "廚房", price: 390, rating: 4.0, color: "#a855f7" }, { id: 12, name: "手錶", category: "服飾", price: 3150, rating: 4.8, color: "#64748b" }, ] const categories = ["全部", "3C 家電", "廚房", "服飾"] // 每頁顯示的數量 const pageSize = 6 // 一個商品的卡片 function ProductCard({ product }: { product: Product }) { return ( <div className="overflow-hidden rounded-lg border bg-card"> <div className="h-24" style={{ backgroundColor: product.color }} /> <div className="p-3"> <p className="text-xs text-muted-foreground">{product.category}</p> <p className="mt-0.5 text-sm font-medium">{product.name}</p> <div className="mt-2 flex items-center justify-between"> <p className="text-sm font-semibold">NT${product.price.toLocaleString()}</p> <p className="text-xs text-amber-500">★ {product.rating}</p> </div> </div> </div> ) } export default function App() { // ① 畫面要記住的值(state) const [query, setQuery] = useState("") const [category, setCategory] = useState("全部") const [sort, setSort] = useState("recommended") const [page, setPage] = useState(1) // ② 要顯示的商品:篩選 → 排序 → 分頁 const filtered = products.filter((product) => product.name.includes(query) && (category === "全部" || product.category === category)) // 價格由低到高 let sorted = filtered if (sort === "cheap") sorted = [...filtered].sort((a, b) => a.price - b.price) // 價格由高到低 if (sort === "expensive") sorted = [...filtered].sort((a, b) => b.price - a.price) // 評分由高到低 if (sort === "rating") sorted = [...filtered].sort((a, b) => b.rating - a.rating) // 根據篩選後的件數計算總頁數 const totalPages = Math.ceil(sorted.length / pageSize) // 第 1 頁 → slice(0, 6),第 2 頁 → slice(6, 12) const shown = sorted.slice((page - 1) * pageSize, page * pageSize) //console.log(query, category, shown) return ( <div className="mx-auto my-6 w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground"> {/* 搜尋與排序 */} <div className="flex flex-wrap items-center gap-2"> {/* 輸入框左側的線條放大鏡圖示 */} <div className="relative flex-1"> <SearchIcon className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <Input value={query} onChange={(e) => { setQuery(e.target.value) setPage(1) // 搜尋後回到第 1 頁 }} className="pl-9" placeholder="搜尋商品名稱" /> </div> <Select value={sort} onValueChange={(value) => { setSort(value) setPage(1) // 變更排序後回到第 1 頁 }}> <SelectTrigger className="w-40"> <SelectValue /> </SelectTrigger> <SelectContent position="popper"> <SelectItem value="recommended">綜合推薦</SelectItem> <SelectItem value="cheap">價格由低到高</SelectItem> <SelectItem value="expensive">價格由高到低</SelectItem> <SelectItem value="rating">評分最高</SelectItem> </SelectContent> </Select> </div> {/* 分類 */} <div className="mt-3 flex flex-wrap gap-2"> {categories.map((name) => ( <Button key={name} size="sm" variant={ category === name ? "default" : "outline" } onClick={() => { setCategory(name) setPage(1) // 變更分類後回到第 1 頁 }} className="rounded-full"> {name} </Button> ))} </div> <p className="mt-4 text-xs text-muted-foreground">共 {sorted.length} 件</p> {/* 商品卡片 */} {sorted.length === 0 ? ( <p className="py-10 text-center text-sm text-muted-foreground">找不到符合的商品</p> ) : ( <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-3"> {shown.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> )} {/* 分頁 */} <div className="mt-5 flex items-center justify-center gap-1"> <Button size="sm" variant="outline" disabled={page === 1} onClick={page === 1 ? undefined : () => setPage(page - 1)}> 上一頁 </Button> {Array.from({ length: totalPages }, (_, i) => i + 1).map ((number) => ( <Button key={number} size="sm" variant={page === number ? "default" : "outline"} onClick={() => setPage(number)}> {number} </Button> ))} <Button size="sm" variant="outline" disabled={page >= totalPages} onClick={() => setPage(page + 1)}> 下一頁 </Button> </div> </div> ) }