Skip to Content
🎲 欢迎使用 RandBox - 功能强大的 JavaScript 随机数据生成库! 了解详情

ScratchCard - 刮刮卡

ScratchCard 是一个基于Canvas的交互式刮刮卡组件,提供真实的刮除体验。支持横排、竖排、对角线等多种中奖模式,以及自定义符号和中奖概率配置。

📦 导入

import { ScratchCard } from '@randbox/react'; import type { ScratchCardProps, ScratchCardResult } from '@randbox/react';

🎯 类型定义

ScratchCardProps

interface ScratchCardProps { // 可选属性 rows?: number; // 行数,默认3 cols?: number; // 列数,默认3 symbols?: string[]; // 符号数组,用于填充卡片 winProbability?: number; // 中奖概率,0-1之间,默认0.3 width?: number; // Canvas 宽度,默认300 height?: number; // Canvas 高度,默认200 // 基础属性(继承自BaseGameProps) className?: string; // CSS类名 style?: React.CSSProperties; // 内联样式 disabled?: boolean; // 是否禁用 // 回调函数 onGameStart?: () => void; // 游戏开始回调 onGameEnd?: (result: ScratchCardResult) => void; // 游戏结束回调 onScratch?: (result: ScratchCardResult) => void; // 刮开回调 onNewCard?: () => void; // 新卡片回调 }

ScratchCardResult

interface ScratchCardResult { grid: string[][]; // 卡片网格内容 isWinner: boolean; // 是否中奖 scratchProgress?: number; // 刮开进度(0-1) winningInfo?: { // 中奖信息 pattern: string[]; // 中奖模式(连线类型) name: string; // 中奖名称 prize: string; // 奖品名称 symbol?: string; // 中奖符号 type?: 'row' | 'col' | 'diagonal'; // 中奖类型:行/列/对角线 positions: Array<{ // 中奖位置数组 row: number; col: number; }>; }; }

🚀 基础用法

import React from 'react'; import { ScratchCard } from '@randbox/react'; function BasicScratchCard() { const handleScratch = (result) => { console.log('刮奖结果:', result); if (result.isWinner) { alert(`🎉 恭喜中奖!获得 ${result.winningInfo.prize}`); } }; return ( <ScratchCard onScratch={handleScratch} /> ); }

🎯 高级用法

自定义网格和符号

function CustomScratchCard() { const symbols = ['🍎', '🍊', '🍋', '🍒', '🍇', '💎', '⭐', '🔔']; return ( <ScratchCard rows={4} cols={4} symbols={symbols} winProbability={0.3} onScratch={(result) => { console.log('自定义刮奖:', result); if (result.isWinner) { const { winningInfo } = result; alert(`中奖类型: ${winningInfo.name} - ${winningInfo.prize}`); } }} /> ); }

多种中奖模式展示

function WinningModesScratchCard() { const [winningMode, setWinningMode] = useState('all'); const [lastResult, setLastResult] = useState(null); const symbols = ['🎯', '🎮', '🎲', '🎪', '🎨', '🎭', '🎺', '🎸']; const handleScratch = (result) => { setLastResult(result); if (result.isWinner) { const { type, name, prize } = result.winningInfo; console.log(`中奖模式: ${type}, 名称: ${name}, 奖品: ${prize}`); } }; return ( <div style={{ padding: '20px' }}> <div style={{ marginBottom: '20px' }}> <label>选择中奖模式: </label> <select value={winningMode} onChange={(e) => setWinningMode(e.target.value)}> <option value="all">全部模式</option> <option value="row">横排</option> <option value="col">竖排</option> <option value="diagonal">对角线</option> </select> </div> <ScratchCard rows={3} cols={3} symbols={symbols} winProbability={0.4} onScratch={handleScratch} key={winningMode} // 重新生成卡片 /> {lastResult && ( <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f5f5f5' }}> <h4>最近刮奖结果:</h4> <div>是否中奖: {lastResult.isWinner ? '是' : '否'}</div> <div>刮除进度: {(lastResult.scratchProgress * 100).toFixed(1)}%</div> {lastResult.isWinner && ( <div style={{ color: 'green' }}> 中奖信息: {lastResult.winningInfo.name} - {lastResult.winningInfo.prize} </div> )} </div> )} </div> ); }

带统计功能的刮刮卡

function StatsScratchCard() { const [stats, setStats] = useState({ totalCards: 0, wins: 0, losses: 0, totalPrize: 0 }); const [currentCard, setCurrentCard] = useState(0); const symbols = ['💰', '💎', '🏆', '🎁', '⭐', '🍀', '🎯', '💯']; const handleNewCard = () => { setCurrentCard(prev => prev + 1); }; const handleScratch = (result) => { setStats(prev => ({ ...prev, totalCards: prev.totalCards + 1, wins: result.isWinner ? prev.wins + 1 : prev.wins, losses: result.isWinner ? prev.losses : prev.losses + 1, totalPrize: result.isWinner ? prev.totalPrize + 100 : prev.totalPrize })); if (result.isWinner) { alert(`🎉 中奖了!奖品价值: $100`); } }; const winRate = stats.totalCards > 0 ? ((stats.wins / stats.totalCards) * 100).toFixed(1) : '0.0'; return ( <div style={{ padding: '20px' }}> {/* 统计面板 */} <div style={{ marginBottom: '20px', padding: '15px', backgroundColor: '#e8f4fd', borderRadius: '10px', textAlign: 'center' }}> <h3>刮奖统计</h3> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '10px' }}> <div>总卡数: {stats.totalCards}</div> <div>中奖数: {stats.wins}</div> <div>未中奖: {stats.losses}</div> <div>中奖率: {winRate}%</div> <div>总奖金: ${stats.totalPrize}</div> </div> </div> <div style={{ textAlign: 'center', marginBottom: '20px' }}> <h4>第 {currentCard + 1} 张刮刮卡</h4> </div> <ScratchCard key={currentCard} rows={3} cols={3} symbols={symbols} winProbability={0.25} onScratch={handleScratch} onNewCard={handleNewCard} /> <div style={{ textAlign: 'center', marginTop: '20px' }}> <button onClick={handleNewCard} style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }} > 购买新卡片 </button> </div> </div> ); }

主题化刮刮卡

function ThemedScratchCard() { const [theme, setTheme] = useState('fruit'); const themes = { fruit: { symbols: ['🍎', '🍊', '🍋', '🍒', '🍇', '🥝', '🍓', '🍑'], background: '#fff3e0', name: '水果主题' }, gem: { symbols: ['💎', '💍', '👑', '🏆', '⭐', '✨', '🌟', '💫'], background: '#f3e5f5', name: '宝石主题' }, animal: { symbols: ['🐱', '🐶', '🐸', '🐯', '🦁', '🐻', '🐼', '🐨'], background: '#e8f5e8', name: '动物主题' } }; const currentTheme = themes[theme]; return ( <div style={{ padding: '20px' }}> <div style={{ marginBottom: '20px' }}> <label>选择主题: </label> <select value={theme} onChange={(e) => setTheme(e.target.value)}> {Object.entries(themes).map(([key, themeData]) => ( <option key={key} value={key}>{themeData.name}</option> ))} </select> </div> <div style={{ padding: '20px', backgroundColor: currentTheme.background, borderRadius: '15px', border: '2px solid #ddd' }} > <h4 style={{ textAlign: 'center', marginBottom: '20px' }}> {currentTheme.name}刮刮卡 </h4> <ScratchCard key={theme} rows={3} cols={3} symbols={currentTheme.symbols} winProbability={0.3} onScratch={(result) => { if (result.isWinner) { alert(`🎉 ${currentTheme.name}中奖了!`); } }} /> </div> </div> ); }

渐进式奖池刮刮卡

function ProgressiveScratchCard() { const [jackpot, setJackpot] = useState(1000); const [cardPrice] = useState(10); const [balance, setBalance] = useState(100); const [cardCount, setCardCount] = useState(0); const symbols = ['💰', '💵', '💴', '💶', '💷', '🏦', '💳', '📈']; const handleNewCard = () => { if (balance < cardPrice) { alert('余额不足!'); return; } setBalance(prev => prev - cardPrice); setJackpot(prev => prev + 5); // 每张卡片增加5元奖池 setCardCount(prev => prev + 1); }; const handleScratch = (result) => { if (result.isWinner) { const winAmount = Math.random() > 0.95 ? jackpot : Math.floor(Math.random() * 100) + 20; if (winAmount === jackpot) { alert(`🎰 超级大奖!获得累积奖池 $${jackpot}!`); setJackpot(1000); // 重置奖池 } else { alert(`🎉 中奖!获得 $${winAmount}!`); } setBalance(prev => prev + winAmount); } }; return ( <div style={{ padding: '20px' }}> {/* 游戏信息面板 */} <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px', marginBottom: '20px', textAlign: 'center' }}> <div style={{ padding: '15px', backgroundColor: '#fff3cd', borderRadius: '10px' }}> <h4>累积奖池</h4> <div style={{ fontSize: '24px', color: '#d4a853' }}>${jackpot.toLocaleString()}</div> </div> <div style={{ padding: '15px', backgroundColor: '#d1ecf1', borderRadius: '10px' }}> <h4>账户余额</h4> <div style={{ fontSize: '24px', color: '#0c5460' }}>${balance}</div> </div> <div style={{ padding: '15px', backgroundColor: '#d4edda', borderRadius: '10px' }}> <h4>已购卡片</h4> <div style={{ fontSize: '24px', color: '#155724' }}>{cardCount}</div> </div> </div> <div style={{ textAlign: 'center', marginBottom: '20px' }}> <div style={{ marginBottom: '10px' }}> 卡片价格: ${cardPrice} | 每张卡片增加${(cardPrice * 0.5)}奖池 </div> <button onClick={handleNewCard} disabled={balance < cardPrice} style={{ padding: '12px 24px', backgroundColor: balance >= cardPrice ? '#28a745' : '#6c757d', color: 'white', border: 'none', borderRadius: '8px', cursor: balance >= cardPrice ? 'pointer' : 'not-allowed', fontSize: '16px' }} > 购买新卡片 (${cardPrice}) </button> </div> {cardCount > 0 && ( <ScratchCard key={cardCount} rows={3} cols={3} symbols={symbols} winProbability={0.4} onScratch={handleScratch} /> )} {cardCount === 0 && ( <div style={{ textAlign: 'center', padding: '40px', backgroundColor: '#f8f9fa', borderRadius: '10px', color: '#6c757d' }}> 购买您的第一张刮刮卡开始游戏 </div> )} </div> ); }

📋 API参考

ScratchCardProps

属性类型默认值描述
rowsnumber3网格行数
colsnumber3网格列数
symbolsstring[]['🍎', '🍊', '🍋', '🍒']可用符号数组
winProbabilitynumber0.2中奖概率(0-1之间)
onScratch(result: ScratchCardResult) => voidundefined刮除完成回调
onNewCard() => voidundefined新卡片生成回调

继承的BaseGameProps

属性类型默认值描述
classNamestring''CSS类名
styleReact.CSSProperties{}内联样式
disabledbooleanfalse是否禁用
onGameStart() => voidundefined游戏开始回调
onGameEnd(result: ScratchCardResult) => voidundefined游戏结束回调

ScratchCardResult

interface ScratchCardResult { grid: string[][]; // 卡片网格数据 isWinner: boolean; // 是否中奖 scratchProgress?: number; // 刮除进度(0-1) winningInfo?: { pattern: string[]; // 中奖图案 name: string; // 中奖类型名称 prize: string; // 奖品描述 symbol?: string; // 中奖符号 type?: 'row' | 'col' | 'diagonal'; // 中奖类型 positions: Array<{ // 中奖位置 row: number; col: number; }>; }; }

🎨 样式定制

容器样式

.scratch-card-container { border: 3px solid #ffc107; border-radius: 15px; box-shadow: 0 8px 25px rgba(255, 193, 7, 0.3); background: linear-gradient(145deg, #fff9c4, #ffecb5); } .scratch-card-container:hover { transform: scale(1.02); transition: transform 0.3s ease; }

中奖效果

.winning-card { animation: winPulse 2s ease-in-out infinite; border-color: #28a745; box-shadow: 0 0 30px rgba(40, 167, 69, 0.5); } @keyframes winPulse { 0%, 100% { box-shadow: 0 0 30px rgba(40, 167, 69, 0.5); } 50% { box-shadow: 0 0 50px rgba(40, 167, 69, 0.8); } }

🔧 高级功能

自定义中奖规则

function CustomWinRules() { const symbols = ['A', 'B', 'C', 'D', 'E']; // 自定义中奖检测逻辑 const checkCustomWinning = (grid) => { // 检查特殊图案 const center = grid[1][1]; const corners = [grid[0][0], grid[0][2], grid[2][0], grid[2][2]]; // 规则1: 中心+四角相同 if (corners.every(symbol => symbol === center)) { return { isWinner: true, type: 'center_corners', name: '中心四角', prize: '特殊奖励' }; } // 规则2: X形图案 if (grid[0][0] === grid[1][1] && grid[1][1] === grid[2][2] && grid[0][2] === grid[1][1] && grid[1][1] === grid[2][0]) { return { isWinner: true, type: 'x_pattern', name: 'X图案', prize: '大奖' }; } return { isWinner: false }; }; return ( <ScratchCard rows={3} cols={3} symbols={symbols} onScratch={(result) => { const customResult = checkCustomWinning(result.grid); if (customResult.isWinner) { alert(`特殊中奖: ${customResult.name} - ${customResult.prize}`); } }} /> ); }

动画增强

function AnimatedScratchCard() { const [isScratching, setIsScratching] = useState(false); const [lastWin, setLastWin] = useState(null); const handleGameStart = () => { setIsScratching(true); }; const handleGameEnd = (result) => { setIsScratching(false); if (result.isWinner) { setLastWin(result.winningInfo); // 延迟显示中奖动画 setTimeout(() => setLastWin(null), 3000); } }; return ( <div style={{ position: 'relative' }}> <ScratchCard rows={3} cols={3} symbols={['🎯', '🎮', '🎲', '🎪']} className={isScratching ? 'scratching' : ''} onGameStart={handleGameStart} onGameEnd={handleGameEnd} /> {lastWin && ( <div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: 'rgba(40, 167, 69, 0.9)', color: 'white', padding: '20px', borderRadius: '10px', textAlign: 'center', animation: 'bounceIn 0.5s' }}> 🎉 {lastWin.name} 🎉 <br /> {lastWin.prize} </div> )} </div> ); }

🎯 最佳实践

1. 中奖概率设计

// 合理的中奖概率配置 const probabilities = { easy: 0.4, // 简单模式,40%中奖率 normal: 0.25, // 普通模式,25%中奖率 hard: 0.15, // 困难模式,15%中奖率 extreme: 0.05 // 极限模式,5%中奖率 };

2. 符号设计建议

// 推荐的符号组合 const symbolSets = { // 高对比度,易区分 classic: ['🔴', '🟢', '🔵', '🟡', '🟣', '🟠', '⚫', '⚪'], // 主题化符号 casino: ['🎰', '🃏', '🎲', '💰', '💎', '🏆', '⭐', '🍀'], // 避免使用相似符号 bad: ['😀', '😃', '😄', '😁'] // 太相似,用户难以区分 };

3. 错误处理

function SafeScratchCard() { const [error, setError] = useState(null); const handleError = (error) => { setError(error.message); console.error('刮刮卡错误:', error); }; return ( <div> {error && ( <div style={{ color: 'red', marginBottom: '10px' }}> 错误: {error} </div> )} <ScratchCard rows={3} cols={3} winProbability={0.3} onScratch={(result) => { setError(null); console.log('刮奖成功:', result); }} /> </div> ); }

🐛 常见问题

Q: 如何实现真正的随机性?

A: 组件使用RandBox的Mersenne Twister算法,提供高质量的随机数生成,确保每次刮奖都是真正随机的。

Q: 可以自定义刮除手势吗?

A: 目前组件支持鼠标和触摸刮除,刮除区域和敏感度可以通过Canvas事件进行调整。

Q: 如何控制中奖概率?

A: 通过winProbability属性设置,范围是0-1,例如0.3表示30%的中奖概率。

Q: 支持哪些中奖模式?

A: 支持横排、竖排、对角线三种基础模式,也可以通过自定义逻辑实现更复杂的中奖规则。

🔗 相关链接

最后更新于: