團購成團進度(React)
以人數計的團購卡 React 元件:已跟團頭像堆疊、階梯團購價、成團倒數,達門檻瞬間 CTA 變「已成團」並整卡脈動噴彩帶,截止未達門檻則轉為未成團並顯示全額退款提示。
台灣社群團、辦公室團最常見的「湊人數」介面:商品列顯示目前團購價——跟團人數每跨過一個階梯門檻,舊價劃線、新價由下往上滾入;進度條以人數為刻度,每個階梯門檻都有節點與價籤;已跟團者以名字首字+確定性色相的頭像堆疊呈現(最多 6 位+「+N」,新成員從右側滑入);成團倒數每秒更新。按「+1 跟團」呼叫 onJoin,達成團門檻的瞬間 CTA 變成「已成團 🎉」、整卡輕微 scale 脈動並噴出彩帶粒子;截止仍未達門檻則自動轉為未成團,顯示「未成團將全額退款」。狀態支援受控/非受控。
載入預覽⋯
npx shadcn@latest add https://webberui.com/r/group-buy-progress.jsonPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
5
<GroupBuyProgress />
安裝
npx shadcn@latest add https://webberui.com/r/group-buy-progress.json或在 components.json 設定 registries 後,改用 @webberui/group-buy-progress 安裝。
使用
import * as React from "react";
import {
GroupBuyProgress,
type GroupBuyParticipant,
} from "@/components/ui/group-buy-progress";
function OfficeCoffeeGroupBuy() {
// 名單由你的資料層維護;元件只讀取,按「+1 跟團」時透過 onJoin 通知你新增
const [participants, setParticipants] = React.useState<GroupBuyParticipant[]>([
{ id: "u1", name: "林曉彤" },
{ id: "u2", name: "陳建宏" },
{ id: "u3", name: "黃雅琪" },
]);
return (
<GroupBuyProgress
product={{ name: "手沖濾掛咖啡 12 入組", subtitle: "深焙", price: 520 }}
tiers={[
{ count: 5, price: 450 },
{ count: 10, price: 399 },
]}
participants={participants}
deadline="2026-09-30T23:59:59+08:00"
maxSpots={20}
onJoin={() =>
setParticipants((list) => [...list, { id: "me", name: "我" }])
}
onFormed={() => console.log("成團了,通知主購結單")}
onChange={(status) => console.log("狀態:", status)}
/>
);
}Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
product | GroupBuyProduct | — | 商品名稱、原價與副標 |
tiers | GroupBuyTier[] | — | 階梯團購價(依 count 遞增;元件內會再排序並忽略非正數) |
participants | GroupBuyParticipant[] | — | 目前已跟團的名單;人數、進度、價格與頭像都由此推導 |
deadline | string | number | Date | — | 成團截止時間(ISO 字串、毫秒時間戳或 Date);到期仍未達門檻即轉為未成團 |
maxSpots | number | — | 名額上限;達到後跟團按鈕變為「已額滿」,同時作為進度條的滿刻度 |
minCount | number | tiers 最小的 count | 成團門檻人數;成團門檻與最低價格階不同時可單獨指定 |
value | GroupBuyStatus | — | 受控狀態;提供時由外部決定目前是募集中/已成團/未成團 |
defaultValue | GroupBuyStatus | 依人數推導 | 非受控模式的初始狀態;省略時已達門檻即為 "formed",否則 "open" |
onChange | (status: GroupBuyStatus) => void | — | 狀態需要變更時回呼(達門檻→formed、截止未達→failed);受控模式請在此更新 value |
onJoin | () => void | — | 按下跟團按鈕時回呼;元件不會自行新增名單,請在此把新成員加進 participants |
onFormed | () => void | — | 狀態實際轉為已成團時回呼一次 |
currency | string | "NT$" | 幣別前綴,顯示在所有金額前 |
maxAvatars | number | 6 | 頭像堆疊最多顯示幾位(顯示最新加入者),其餘彙整成「+N」 |
showCountdown | boolean | true | 是否顯示成團倒數 |
compact | boolean | false | 精簡模式:更小的間距與頭像,隱藏副標、階梯價籤與提示行,適合嵌進商品列表 |
joinLabel | string | — | 跟團按鈕文字;等同 labels.join 的捷徑,兩者同時提供時以此為準 |
labels | Partial<GroupBuyProgressLabels> | 內建繁中 | 覆寫介面文案(部分覆寫即可,其餘沿用內建) |
className | string | — | 透傳到最外層容器 |
GroupBuyStatus
"open" | "formed" | "failed"——募集中/已成團/未成團。
GroupBuyProduct
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
name | string | — | 商品名稱 |
price | number | — | 原價:未達任何階梯門檻時顯示的單價 |
subtitle | string | — | 副標/規格說明(精簡模式不顯示) |
GroupBuyTier
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
count | number | — | 門檻人數(正整數) |
price | number | — | 達到門檻後的單價 |
GroupBuyParticipant
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 唯一識別,同時作為頭像色相的種子(同一 id 每次顏色相同) |
name | string | — | 顯示名稱;頭像取首字 |
color | string | — | 自訂頭像底色(任意 CSS 色值);省略時由 id 推導確定性色相 |
GroupBuyProgressLabels
含 {count}/{price} 的字串會在顯示時代入實際值。
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
join | string | "+1 跟團" | 跟團按鈕文字 |
formed | string | "已成團 🎉" | 成團後的按鈕文字 |
failed | string | "未成團" | 未成團的按鈕文字,也用於進度列右側 |
full | string | "已額滿" | 額滿的按鈕文字 |
refundNote | string | "未成團將全額退款" | 未成團時顯示於按鈕下方的退款提示 |
formedNote | string | "已成團,等待主購結單" | 成團後顯示於進度列右側的提示 |
joined | string | "{count} 人已跟團" | 已跟團人數 |
untilFormed | string | "再 {count} 人成團" | 距離成團還差幾人 |
nextTier | string | "再 {count} 人降至 {price}" | 距離下一階還差幾人(顯示於價格下方) |
bestPrice | string | "已達最低團購價" | 已達最後一階時的提示 |
original | string | "原價" | 劃線價為原價時的前綴 |
countdown | string | "成團倒數" | 倒數區標題 |
ended | string | "已截止" | 截止後倒數區的文字 |
tier | string | "{count} 人" | 進度條上階梯節點的人數籤 |
threshold | string | "成團" | 成團門檻節點的籤(門檻與任何階梯人數都不同時才顯示) |
more | string | "還有 {count} 人" | 頭像堆疊「+N」的無障礙文字 |
day / hour / minute / second | string | "天" / "時" / "分" / "秒" | 倒數單位 |
另外具名匯出 resolveGroupBuyTier(tiers, count)(回傳目前落在的階與下一階)與 formatGroupBuyPrice(amount, currency)(千分位金額字串),方便在購物車或結帳頁沿用同一套價格規則。
細節
- 價格落階:目前單價 = 已達到的最高階價格,未達任何階時為
product.price。價格一變,前一個價格就成為劃線舊價(初始已達階時舊價為原價),新價從下方滾入 - 進度刻度:滿刻度為
maxSpots;未提供時取最高階人數與成團門檻的較大者。每個階梯在軌道上都有節點與「人數/價格」籤;兩端的籤自動靠邊對齊,不會凸出軌道 - 成團與未成團:非受控模式下,募集中人數達
minCount即自動轉為formed;deadline到期仍未達門檻則轉為failed。受控模式元件只透過onChange建議下一個狀態,由你決定是否更新value。成團與未成團都是終態,之後不會再自動改變 - 倒數:
Date.now()只在 effect 內讀取;SSR 與首次渲染顯示--佔位,掛載後才每秒更新,避免水合不一致。無效的deadline會隱藏倒數且永不到期 - 頭像:不用圖片;底色由
id雜湊出色相(同一人每次相同),也可用color指定。堆疊顯示最新加入的maxAvatars位,較早的成員彙整為「+N」
可及性
- 進度條為
role="progressbar",aria-valuenow/aria-valuemax以人數為單位,aria-valuetext朗讀「N 人已跟團」;人數文字置於aria-live="polite"區域,跟團後會被螢幕閱讀器朗讀 - 倒數為
role="timer",aria-label提供完整的「天/時/分/秒」文字;未成團的退款提示為role="status" - 頭像堆疊為語意
list,每位成員的li帶名字,「+N」帶「還有 N 人」;視覺首字對輔助科技隱藏 - 跟團按鈕為原生
button(type="button"),成團/未成團/額滿時disabled並同步aria-disabled;鍵盤操作與focus-visible外框沿用瀏覽器原生行為 - 使用者系統開啟「減少動態效果」時,停用價格滾動、頭像滑入、整卡脈動與彩帶粒子,只保留即時的文字與顏色變化