出單機收據
付款完成後模擬熱感應出單機逐行送紙的收據元件:處理中、出單、完成三階段,紙底鋸齒、可撕下與重播。
付款成功的那一刻,讓收據像熱感應出單機一樣一格一格吐出來。元件分成處理中、出單、完成三個階段:處理中時機身指示燈脈動、小螢幕顯示狀態;出單時紙張以 20 段頓挫從出紙口送出(也可切成一段平滑送出);完成後收據可以按鈕或往下拖曳撕下,紙張帶著隨機旋轉掉落。紙底鋸齒用 clip-path 生成、齒數可調,機身與紙面質感全由 CSS 漸層與內嵌 SVG 噪點組成,不引用任何外部圖檔。收據內容就是 children,品項、稅額、訂單號怎麼排都由你決定。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add "https://webberui.com/r/receipt-printer.json?t=<安裝 token>"Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<ReceiptPrinter/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
安裝
npx webberui@latest add "https://webberui.com/r/receipt-printer.json?t=<安裝 token>"或在 components.json 設定 registries 後,改用 @webberui/receipt-printer 安裝。
使用
import * as React from "react";
import {
ReceiptPrinter,
type ReceiptStage,
} from "@/components/ui/receipt-printer";
export function CheckoutReceipt() {
const [stage, setStage] = React.useState<ReceiptStage>("idle");
async function pay() {
setStage("processing");
await chargeCard(); // 你的付款 API
setStage("printing");
}
return (
<>
<button type="button" onClick={pay}>
確認付款
</button>
<ReceiptPrinter stage={stage} onStageChange={setStage}>
<p className="text-center font-bold">拾光雜貨</p>
<dl className="mt-2 space-y-1 border-t border-dashed pt-2">
<div className="flex justify-between">
<dt>年度訂閱方案</dt>
<dd>NT$ 2,880</dd>
</div>
<div className="flex justify-between font-semibold">
<dt>合計</dt>
<dd>NT$ 3,024</dd>
</div>
</dl>
</ReceiptPrinter>
</>
);
}不需要接付款流程時,直接用非受控模式:掛載後停留 processingMs 自動出單,再用 ref 的 replay() 重播。
const ref = React.useRef<ReceiptPrinterHandle>(null);
<ReceiptPrinter ref={ref} mode="smooth" duration={2.4} teeth={28}>
{/* 收據明細 */}
</ReceiptPrinter>;
ref.current?.replay();需要自訂排版(例如把狀態列放在別處)時改用複合元件:
<ReceiptPrinter.Root stage={stage} onStageChange={setStage}>
<ReceiptPrinter.Machine>
<ReceiptPrinter.Header>TP-58</ReceiptPrinter.Header>
<ReceiptPrinter.Screen />
</ReceiptPrinter.Machine>
<ReceiptPrinter.Output>
<ReceiptPrinter.Paper>{/* 收據明細 */}</ReceiptPrinter.Paper>
</ReceiptPrinter.Output>
<ReceiptPrinter.Status className="mt-3" />
</ReceiptPrinter.Root>Props
ReceiptPrinter 與 ReceiptPrinter.Root 接受相同的 props(ReceiptPrinter 的 children 會放進紙張本體,Root 的 children 則是自由組合的子元件)。
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
stage | "idle" | "processing" | "printing" | "complete" | — | 受控的階段;未提供時由元件內部管理 |
defaultStage | ReceiptStage | "processing" | 非受控模式的初始階段 |
onStageChange | (stage: ReceiptStage) => void | — | 階段變更時回呼;出紙結束會以 "complete" 通知,受控時由父層決定是否採用 |
mode | "stepped" | "smooth" | "stepped" | stepped 以 20 段頓挫逐行送紙;smooth 一段 ease-in-out 送出 |
duration | number | 1.75 | 出紙總長(秒);未提供時讀取 CSS 變數 --wb-receipt-duration |
teeth | number | 40 | 紙底鋸齒的齒數,齒深固定 4px |
processingMs | number | 1200 | 非受控時 processing 階段停留的毫秒數,之後自動進入 printing |
tearable | boolean | true | 完成後是否可撕下(按鈕或往下拖曳超過 60px) |
onTear | () => void | — | 撕下動畫結束後回呼 |
labels | ReceiptPrinterLabels | — | 覆寫介面與螢幕閱讀器文案 |
grain | boolean | false | 機身與紙面額外疊一層顆粒質感 |
ref | React.Ref<ReceiptPrinterHandle> | — | 取得 replay():把收據收回機內、階段回到 processing |
className | string | — | 透傳到最外層容器 |
子元件 Machine、Header、Output、Paper 接受 children 與 className;Screen 與 Status 只接受 className。
ReceiptPrinterLabels
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
device | string | "出單機" | 機身頂部的品名(Header 沒有 children 時顯示) |
idle | string | "待命" | 待命階段的狀態文字 |
processing | string | "處理中…" | 處理中階段的狀態文字 |
printing | string | "列印收據…" | 出單階段的狀態文字 |
complete | string | "完成" | 完成階段的狀態文字 |
srProcessing | string | "正在處理付款" | 螢幕閱讀器播報:處理中 |
srPrinting | string | "正在列印收據" | 螢幕閱讀器播報:出單中 |
srComplete | string | "收據已完成" | 螢幕閱讀器播報:完成 |
srTorn | string | "收據已撕下" | 螢幕閱讀器播報:已撕下 |
tear | string | "撕下收據" | 撕下按鈕文字 |
paper | string | "收據" | 紙張區塊的 aria-label |
另外具名匯出 ReceiptStage、ReceiptFeedMode、ReceiptPrinterHandle、ReceiptPrinterProps 與 ReceiptPrinterRootProps 型別。
細節
- 逐行頓挫:
stepped模式把 0→100% 的送紙切成 20 段,keyframes 共 41 個值、「前進、停住」交替,times 均分、ease 為 linear,前進與停頓各佔一半時間,看起來就是熱感應機一格一格吐紙 - 出紙口:紙張初始
translateY(-100%)藏在出紙口內,出單時移到 0。出紙口用clip-path: inset()只裁上緣,撕下時的旋轉、下墜與陰影才不會被切掉 - 受控與非受控:非受控時 processing 停留
processingMs後自動出單、出紙結束自動完成;受控時元件只在出紙結束時以onStageChange("complete")通知,何時從 processing 進入 printing 由父層決定(通常是付款 API 回應成功之後) - 節奏變數:
--wb-receipt-duration(出紙總長)、--wb-receipt-accent(指示燈與完成圖示,預設#34d399)、--wb-receipt-processing(處理中指示燈,預設#fbbf24)、--wb-receipt-screen(小螢幕文字,預設#bef264) - 撕下:旋轉角在撕下當下於 −8° 到 8° 之間隨機產生,紙張以 500ms ease-in 往下 120%、淡出;回到 processing 或呼叫
replay()時自動復原 - 深色模式:紙面維持淺色紙張(
neutral-100),機身本來就是深色,兩種主題下都像真實的出單機
可及性
- Root 內有一個
sr-only的role="status"、aria-live="polite"區域,依階段播報「正在處理付款」「正在列印收據」「收據已完成」,撕下後播報「收據已撕下」;Screen 與 Status 的視覺文字為aria-hidden,避免重複朗讀 - 紙張是帶
aria-label="收據"的section,尚未出單或已撕下時設為aria-hidden與inert;明細建議用dl或table標記 - 「撕下收據」是真正的
button,可用 Tab 聚焦、Enter/空白鍵觸發,帶focus-visible外框;拖曳撕下只是額外的指標操作 - 使用者系統開啟「減少動態效果」時,不出紙、不頓挫,進入 printing 就直接顯示完整收據與完成狀態;狀態文字只淡入淡出不位移,指示燈不脈動,撕下改為淡出