直播購物疊層
直播賣場 UI 疊層的 React 元件:固定比例舞台上疊 LIVE 徽章與觀看人數滾動、愛心噴發、彈幕留言流、「+1」喊單自動偵測與浮出計數、置頂商品卡與庫存倒數,可放 children 當真正的影片。
台灣社群直播、電商直播的「喊單」文化:主播介紹商品,觀眾在留言區打「+1」就代表下單。這個元件把整套直播賣場的 UI 疊層封裝成一個固定比例的舞台(直式 9:16 或橫式 16:9)——底層是漸層+緩慢漂移的光暈當作影片占位(也可放 children 當真正的 video),上面疊:左上 LIVE 徽章與逐位滾動的觀看人數、右側愛心按鈕與上飄粒子、左下留言流(新訊息從底部滑入、舊的上移淡出、帳號以色塊首字當頭像)、+1 自動偵測(符合規則的留言高亮,商品卡上方浮出「+1」字樣,訂單數與庫存即時更新)、置頂商品卡與「+1 喊單」按鈕。留言串支援受控/非受控,也可以開 simulate 讓元件自己模擬留言與觀看人數變動;所有帳號、留言與商品皆為虛構示範資料。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add "https://webberui.com/r/live-shopping-overlay.json?t=<安裝 token>"Live Shopping Overlay 是用 React、Tailwind CSS 與 Motion 做的直播購物(直播帶貨、直播喊單)介面疊層元件,圖示來自 lucide-react。它只負責畫面與 +1 判定,留言來源、訂單建立與付款流程都由你的後端處理。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<LiveShoppingOverlay/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 電商平台或品牌官網的直播賣場頁,疊在串流播放器上
- 社群團購、直播喊單的後台預覽,讓主播在開播前確認畫面
- 直播電商產品的提案、展示頁,用
simulate呈現開播後的樣子 - 短影音或直播 App 原型,在網頁上驗證直式 9:16 的互動配置
安裝
npx webberui@latest add "https://webberui.com/r/live-shopping-overlay.json?t=<安裝 token>"或在 components.json 設定 registries 後,改用 @webberui/live-shopping-overlay 安裝。
使用
import {
LiveShoppingOverlay,
isPlusOne,
} from "@/components/ui/live-shopping-overlay";
// 示範模式:留言流與觀看人數由元件內部模擬(商品與帳號皆為虛構)
<LiveShoppingOverlay
simulate
product={{ name: "陶瓷手沖濾杯組", price: 680, stock: 12 }}
onPlusOne={(msg) => console.log("喊單", msg.user, msg.text)}
onHeart={(count) => console.log("愛心", count)}
/>
// 正式接後端:留言受控、觀看人數由 API 提供,舞台底層放真正的 video
<LiveShoppingOverlay
layout="landscape"
messages={messages}
onMessagesChange={setMessages}
viewers={viewers}
product={product}
onPlusOne={(msg) => createOrder(msg)}
>
<video src={streamUrl} autoPlay muted playsInline />
</LiveShoppingOverlay>
// 喊單判定函式可單獨使用(例如在後端或表單層過濾留言)
isPlusOne("+1 黑色"); // true
isPlusOne("我要 1 個"); // true
isPlusOne("有優惠碼嗎"); // false範例
用 WebSocket 接即時留言
受控模式下,新留言由你推進 messages;按「+1 喊單」時元件會呼叫 onMessagesChange 並帶入加上新留言的完整陣列,最後一則就是自己送出的留言。
"use client";
import * as React from "react";
import {
LiveShoppingOverlay,
type LiveMessage,
} from "@/components/ui/live-shopping-overlay";
export function LiveRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = React.useState<LiveMessage[]>([]);
React.useEffect(() => {
const ws = new WebSocket(`wss://example.com/rooms/${roomId}`);
ws.onmessage = (event) => {
const msg: LiveMessage = JSON.parse(event.data);
setMessages((prev) => [...prev, msg].slice(-50));
};
return () => ws.close();
}, [roomId]);
return (
<LiveShoppingOverlay
messages={messages}
onMessagesChange={(next) => {
const mine = next[next.length - 1];
setMessages(next);
void fetch(`/api/rooms/${roomId}/comments`, {
method: "POST",
body: JSON.stringify(mine),
});
}}
product={{ name: "保溫隨行杯 500ml", price: 890, stock: 30 }}
onPlusOne={(msg) => console.log("新喊單", msg.user)}
/>
);
}id 必須唯一,元件以它判斷新留言;同一個 id 不會重複計入喊單。
橫式舞台搭配真正的影片
layout="landscape" 改成 16:9;children 會被拉伸填滿並放在所有疊層下方。
<LiveShoppingOverlay
layout="landscape"
title="週五晚間保養品專場"
selfName="小安"
showViewers={false}
maxMessages={4}
messages={messages}
onMessagesChange={setMessages}
product={product}
>
<video src={streamUrl} autoPlay muted playsInline />
</LiveShoppingOverlay>用 isPlusOne 統計留言
具名匯出的 isPlusOne 和元件內部使用同一套規則,可以在其他客戶端元件裡統計喊單數。
import { isPlusOne } from "@/components/ui/live-shopping-overlay";
const plusOneCount = messages.filter((m) => isPlusOne(m.text)).length;Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
messages | LiveMessage[] | — | 受控留言串(由舊到新);提供時內部狀態與 simulate 失效,新增留言請透過 onMessagesChange 回寫 |
defaultMessages | LiveMessage[] | [] | 非受控模式的初始留言 |
onMessagesChange | (messages: LiveMessage[]) => void | — | 留言串變動時回呼(按「+1 喊單」或模擬新增留言);受控模式下由此更新 messages |
simulate | boolean | false | 非受控時由元件內部以計時器模擬留言流(含隨機 +1);示範用,正式接後端時請關閉 |
product | LiveProduct | 內建虛構商品(NT$680、庫存 20) | 置頂商品(名稱、售價、起始庫存) |
viewers | number | — | 受控觀看人數;未提供時由內部模擬每隔數秒微幅浮動 |
defaultViewers | number | 1280 | 內部模擬觀看人數的起始值(viewers 未提供時才有效) |
showViewers | boolean | true | 是否顯示右上角觀看人數 |
title | string | "週三晚上開賣" | 直播間標題,顯示於 LIVE 徽章右側 |
selfName | string | "我" | 按「+1 喊單」時送出留言所用的帳號名稱 |
onPlusOne | (message: LiveMessage) => void | — | 每偵測到一則新的 +1 留言時回呼一次(含自己按下的喊單) |
onHeart | (count: number) => void | — | 點擊愛心時回呼,帶入累計次數 |
layout | "portrait" | "landscape" | "portrait" | 舞台比例:直式 9:16(手機直播)或橫式 16:9 |
maxMessages | number | 5 | 留言流最多同時顯示的則數,超過的舊留言上移淡出 |
children | React.ReactNode | — | 放在舞台底層的節點(例如真正的 video),會被拉伸填滿並置於疊層之下 |
className | string | — | 透傳到最外層舞台容器 |
LiveMessage
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 唯一識別碼;元件以此判斷「新訊息」,同一個 id 不會重複觸發 +1 |
user | string | — | 發言帳號名稱(頭像取首字,並依名稱雜湊上色) |
text | string | — | 留言內容;符合 +1 規則時自動高亮並計入喊單 |
ts | number | — | 送出時間(毫秒時間戳),選填、僅供呼叫端排序或顯示 |
LiveProduct
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
name | string | — | 商品名稱 |
price | number | — | 售價(新台幣) |
stock | number | — | 起始庫存;每一筆 +1 扣一件,扣到 0 顯示完售並停用喊單按鈕 |
另外具名匯出 isPlusOne(text) 與 LiveShoppingLayout 型別,方便在後端或表單層重複使用同一套喊單判定規則。
細節
- +1 判定規則(
isPlusOne):留言 trim 後符合/(^|\s)\+?1(\s|$)/(獨立的「1」或「+1」,例如「1」「我要 1 個」),或內文任何位置含「+1」/全形「+1」(例如「+1黑色」「幫我留一個+1」) - 訂單數與庫存:每一則「新的」+1 留言扣一件庫存並觸發
onPlusOne;元件以id記錄已處理的留言,同一個id不會重複計數。初始留言(defaultMessages或首次傳入的messages)中的 +1 直接算進起始訂單數,但不觸發回呼與浮出動畫,SSR 與客戶端輸出一致 - 受控/非受控:提供
messages即為受控,元件不再自己新增留言(simulate失效),按「+1 喊單」會以onMessagesChange([...messages, 新留言])回寫,由你決定是否送到後端。非受控時內部保留的則數比maxMessages多一些,動態調大上限還有舊留言可補 - 模擬留言流:以
setTimeout鏈而非固定setInterval排程,每則間隔帶確定性抖動,讀起來像真人留言而非節拍器;約三成五機率喊單,完售後只出一般聊天;分頁切到背景時不塞留言,避免回來瞬間一大串動畫同時播放;卸載時清理計時器 - 觀看人數:
viewers受控或以defaultViewers起始由內部每 3 秒微幅浮動(約 ±3%),數字逐位滾動;千分位格式自行實作,不走toLocaleString,避免伺服器與瀏覽器 ICU 差異造成 hydration 不一致 - 愛心與「+1」浮出粒子皆以 motion 自行實作,每顆的飄移、擺幅、縮放與色調由序號推導的確定性偽亂數算出,render 期間不呼叫
Math.random;粒子壽命結束即移除並清理計時器 - 底層「影片」占位是漸層+兩顆緩慢漂移的光暈,不用外部影片/圖片;傳入
children時會被拉伸填滿(object-cover)並置於所有疊層之下,上下漸暗遮罩確保白字、留言與商品卡在任何底圖上都可讀 - 庫存倒數:商品卡進度條隨每筆 +1 縮短,剩 3 件以內轉紅並顯示「最後 N 件」,扣到 0 顯示「已完售」並停用喊單按鈕
可及性
- 最外層為
role="region"並以aria-label帶入直播間標題;愛心按鈕與「+1 喊單」皆為type="button",具focus-visible環,完售時喊單按鈕disabled - 留言流為
role="log"但aria-live="off"——模擬留言每一兩秒一則,逐則播報會淹沒螢幕閱讀器;只有自己按下「+1 喊單」時,才透過隱形的role="status"區域播報累計單數與剩餘庫存 - 觀看人數不放
aria-live(每隔數秒就變),改以sr-only文字補上「人觀看中」單位;愛心按鈕的aria-label帶入已送出次數 - 所有裝飾性元素(光暈、漸暗遮罩、頭像色塊、粒子、進度條)皆
aria-hidden,留言文字本身仍是可讀的 DOM 內容 - 使用者系統開啟「減少動態效果」時,停用光暈漂移、LIVE 呼吸點、愛心與「+1」粒子、數字滾動與留言滑入動畫,只保留數字、高亮與文字更新;模擬留言與觀看人數變動的間隔放慢一倍
常見問題
元件會建立訂單或處理付款嗎?
不會。偵測到新的 +1 留言時只會呼叫 onPlusOne,訂單建立、庫存檢查與付款都需要在你的後端完成。
畫面上的庫存會與後端同步嗎?
不會自動同步。顯示的庫存是 product.stock 減去留言串中已計入的 +1 筆數,屬於畫面上的即時計數;實際能否成立訂單,請以後端回應為準。
什麼樣的留言算 +1?
留言 trim 後含有獨立的「1」或「+1」(例如「1」「我要 1 個」),或任何位置含有「+1」、全形「+1」(例如「+1黑色」)。規則可以透過 isPlusOne 單獨使用。
可以在 Next.js App Router 使用嗎?
可以。檔案開頭已經有 "use client",在 Server Component 頁面裡照一般方式 import 即可。初始留言中的 +1 會直接算進起始訂單數,觀看人數的千分位格式也是自行實作,伺服器與瀏覽器輸出一致。
simulate 可以用在正式環境嗎?
不建議。simulate 會用計時器產生虛構留言與觀看人數變動,只適合示範與原型;接後端時改用受控的 messages 與 viewers。