抽奖界面的设计看似简单,但要做到稳定可靠、便于维护,其中隐藏着不少技术细节。特别是采用 HTML 表格实现时,很多人会下意识地使用 CSS Grid 或 Flexbox 布局,认为“视觉上像网格就行了”。然而,在需要精确控制每个格子(如随机选中、高亮、禁用)的场景下,DOM 结构与逻辑索引必须严格对应,否则后续开发会问题重重。

表格结构必须用 table + tr + td,不能靠 CSS Grid 伪装
抽奖界面最核心的难点在于:视觉上的网格必须与逻辑上的行列索引完美对齐。仅仅看起来像格子是不够的。如果用 div 配合 display: grid 拼凑出“表格”,JavaScript 无法通过 DOM 精确定位到第几行第几列。后续执行随机选中、高亮显示、禁用操作时,定位会变得混乱。
可靠的方案是:直接使用原生 table 标签,每行用 tr,每个奖品格子用 td。这样 DOM 层级天然反映了二维坐标,rowIndex 和 cellIndex 可以直接读取,既省心又精确。
- 不要在
td内部嵌套div再填充内容——除非只是为了装饰,否则会干扰点击区域和事件绑定。 - 不要使用
colspan或rowspan合并单元格。抽奖格子必须是一一对应的独立td,否则Math.floor(Math.random() * totalCells)计算出的索引可能落空或重复。 - 固定行列数建议直接写死。例如 4×4 就写 4 个
tr,每个包含 4 个td。动态生成也可以,但最终 DOM 必须是一个规整的矩形,不能有缺块。
给每个 td 绑定唯一标识,别依赖 innerText 或 class 名
抽奖时,必须明确知道“抽中了哪个格子”。靠 td.innerText === "一等奖" 来匹配?这非常不可靠——文本可能重复、包含空格、被翻译,或者后期文案修改。使用 class 名如 prize-1 也不保险,容易误删或冲突。
更稳妥的方式是:使用自定义属性 data-index 或 data-row + data-col,在初始化时就写死。例如:
谢谢参与 再来一次
data-index适合一维遍历,通过table.querySelectorAll('td')后直接按索引取值。data-row和data-col适合二维逻辑,例如禁止相邻格子连续中奖,或者高亮整行。- 千万不要用
id。大量td会导致 ID 重复的风险,而且 JavaScript 查询data-属性比id更快更稳定。
点击触发抽奖时,禁用所有 td 并加 loading 状态
用户手速快、网络延迟、JavaScript 执行慢,都可能造成多次点击,从而抽中多个奖品。最简单的办法不是依赖防抖函数,而是从 DOM 层面即时锁定。
- 点击后立刻遍历所有
td,设置pointer-events: none,并添加一个 class 如disabled。注意td本身不支持disabled属性,所以必须用 CSS 控制。 - 同时,给当前按钮或整个表格加上一个半透明遮罩层(用绝对定位的
div覆盖),从视觉上阻止用户再次点击。 - 中奖后恢复之前,务必重置所有
td的样式,包括背景色、边框、透明度。否则下次抽奖时,视觉状态会错乱。
一个常见错误是:只禁用按钮,不锁定 td;或者使用 setTimeout 模拟抽奖动画,却没有清除定时器,导致多次回调叠加。
中奖高亮要区分「当前中奖格子」和「历史中奖记录」
如果只是单次抽奖,高亮一个格子就够了。但很多需求需要保留历史记录,例如显示“已抽中:3 个二等奖”。这时,不能只修改 background-color,否则颜色叠加起来根本分不清。
- 推荐使用 class 切换:
td.current-win负责动效和边框闪烁,td.history-win负责浅灰底和小图标。逻辑清晰,样式可复用。 - 避免使用内联 style 修改
backgroundColor。这不利于主题切换,也不利于 CSS 覆盖。 - 如果中奖格子需要弹窗展示详情,务必把
data-属性传过去,而不是从 DOM 中重新查询。因为异步操作时,节点可能已被移除或替换。
真正麻烦的是多轮抽奖后,某个格子状态混合:它既是本轮中奖,又是历史中过奖。这时候,class 列表管理比单纯使用 classList.toggle 一刀切要稳妥得多。不要偷懒。
