Vue3 + TypeScript 实战:从零封装一个可复用的九宫格抽奖组件
Vue3 + TypeScript 实战:构建高复用九宫格抽奖组件
去年在开发一个电商促销活动时,产品经理突然提出要在首页增加一个九宫格抽奖模块。当时第一反应是去找现成的轮子,但试了几个开源组件后,发现要么样式定制困难,要么API设计不符合我们的业务逻辑。最终决定自己动手封装一个,没想到这个组件后来被复用到公司三个不同业务线中。今天就来分享如何从零构建一个生产级可复用的九宫格抽奖组件。
1. 组件设计与类型定义
1.1 核心数据结构建模
在TypeScript中,良好的类型定义是组件健壮性的第一道防线。对于抽奖组件,我们需要明确几个核心类型:
interface PrizeItem { id: string | number name: string icon?: string probability?: number // 中奖概率(可选) isSpecial?: boolean // 是否是特殊奖项 } interface LotteryConfig { prizes: PrizeItem[] buttonText?: string roundsBeforeStop?: number // 停止前旋转圈数 animationDuration?: number // 动画时长(ms) highlightColor?: string // 高亮颜色 }这种设计将奖品数据与配置参数分离,使得组件更容易适应不同业务场景。比如在春节活动中可以使用红包图标,而在周年庆时改用奖杯图标。
1.2 组件Props的工程化设计
使用Vue3的defineProps配合TypeScript,我们可以构建出既严格又灵活的props接口:
const props = defineProps<{ config: LotteryConfig disabled?: boolean // 事件回调 onStart?: () => Promise<boolean> // 返回是否允许开始 onEnd?: (prize: PrizeItem) => Promise<void> }>()特别设计的onStart回调返回Promise,这使得我们可以方便地加入前置校验逻辑,比如:
- 检查用户是否登录
- 验证抽奖资格
- 获取后端生成的随机种子
2. 核心逻辑实现
2.1 动画控制系统
九宫格抽奖的核心在于平滑的动画效果和精确的停止控制。我们使用Composition API封装动画逻辑:
const useLotteryAnimation = (config: LotteryConfig) => { const currentActive = ref<number>(-1) const isRunning = ref(false) const transitionSpeed = ref(100) // 顺时针索引序列 const indexSequence = [0, 1, 2, 5, 8, 7, 6, 3] const start = async (targetIndex: number) => { isRunning.value = true let rounds = 0 let currentStep = 0 // 加速阶段 while (transitionSpeed.value > 10) { transitionSpeed.value -= 2 await nextTick() } // 匀速阶段 while (rounds < (config.roundsBeforeStop || 3)) { currentActive.value = indexSequence[currentStep % indexSequence.length] currentStep++ if (currentStep % indexSequence.length === 0) rounds++ await sleep(transitionSpeed.value) } // 减速定位 // ...具体实现省略 } return { currentActive, isRunning, start } }2.2 状态管理与错误处理
一个健壮的抽奖组件需要处理各种边界情况:
const drawingState = reactive({ isDrawing: false, remainingTimes: 0, lastPrize: null as PrizeItem | null }) const startLottery = async () => { if (drawingState.isDrawing || props.disabled) return try { drawingState.isDrawing = true const canStart = await props.onStart?.() if (canStart === false) { throw new Error('Not qualified') } const result = await fetchPrizeFromBackend() await animation.start(result.index) await props.onEnd?.(result.prize) drawingState.lastPrize = result.prize } catch (err) { console.error('抽奖出错:', err) // 显示友好错误提示 } finally { drawingState.isDrawing = false } }3. 可视化与交互优化
3.1 自适应布局方案
九宫格布局看似简单,但要适配不同设备需要精心设计:
.lottery-grid { display: grid; grid-template-columns: repeat(3, 1fr); aspect-ratio: 1/1; max-width: 100vw; .prize-item { position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; &.active { background: v-bind('config.highlightColor || "#ffeb3b"'); transform: scale(0.95); transition: all 0.2s; } } }使用CSS Grid结合aspect-ratio可以确保组件始终保持正方形,而v-bind则允许动态样式配置。
3.2 交互细节打磨
- 按钮防抖:防止快速重复点击
- 动画中断处理:页面隐藏时暂停动画
- 移动端适配:触摸反馈优化
- 无障碍访问:ARIA属性支持
const handleClick = useDebounce(() => { if (!drawingState.isDrawing) { startLottery() } }, 300) onMounted(() => { document.addEventListener('visibilitychange', () => { if (document.hidden) { // 暂停动画逻辑 } }) })4. 业务集成实践
4.1 与后端API的优雅对接
在实际项目中,抽奖逻辑通常需要与后端深度集成。我们设计了一个API适配层:
const useLotteryAPI = () => { const fetchPrize = async (): Promise<{ index: number prize: PrizeItem }> => { const res = await axios.post('/api/lottery/draw', { activityId: props.activityId }) // 将API响应映射到组件需要的格式 return { index: convertPrizeToIndex(res.data.prize), prize: res.data.prize } } return { fetchPrize } }4.2 多场景配置方案
通过预设配置对象,可以快速适配不同业务场景:
const festivalConfig = { prizes: [ { id: 1, name: '春节红包', icon: 'red-packet' }, // ...其他奖品 ], buttonText: '立即抽奖', highlightColor: '#f44336' } const anniversaryConfig = { prizes: [ { id: 1, name: '特等奖', icon: 'trophy' }, // ...其他奖品 ], buttonText: '点击抽奖', roundsBeforeStop: 4 }5. 高级功能扩展
5.1 概率权重系统
对于需要前端计算中奖概率的场景,可以实现权重算法:
const calculateWinner = (prizes: PrizeItem[]) => { const totalWeight = prizes.reduce((sum, p) => sum + (p.probability || 1), 0) let random = Math.random() * totalWeight for (let i = 0; i < prizes.length; i++) { if (random < prizes[i].probability!) { return i } random -= prizes[i].probability! } return 0 }5.2 动画效果增强
通过CSS自定义属性和Transition组合,可以创建更丰富的动画效果:
.prize-item { --highlight-scale: 0.95; &.active { transform: scale(var(--highlight-scale)); box-shadow: 0 0 15px var(--highlight-color); animation: pulse 0.5s infinite alternate; } } @keyframes pulse { from { opacity: 0.8; } to { opacity: 1; } }在最近一次使用中,我们为这个组件添加了WebGL渲染的粒子特效,当用户中大奖时会出现全屏烟花效果。这种渐进式增强的思路既保持了基础功能的可用性,又能为高端设备提供更炫酷的体验。
