用 Vue 3 做了一套年会抽奖工具,顺便踩了些坑

去年为了年会抽奖临时做了这个小工具,今年又被点名用同一套方案。

趁着复用的机会,把之前欠下的一些小坑顺手补了一轮,整理完善之后,就决定顺便发出来给大家也能直接用:

  • 在线地址:https://huyikai.github.io/AnnualRaffle/

下面主要想分享两件事:

  • 这套工具大概能做什么;
  • 为啥我要在 UI 里留一个"临时加奖项"的入口,以及实战中这个入口是怎么救场的。

这个"年会抽奖"大概长什么样

不用太多形容词,就按模块简单说一下。

抽奖视觉

  • 中心是一个 3D 标签云,所有参与抽奖的人会在里面旋转。
  • 每个人可以配头像,抽到的时候会同时展示名字和照片。
  • 没配头像的会用默认头像兜底。

奖项相关

  • 默认有常规奖项:一等奖、二等奖、三等奖、幸运奖之类。
  • 每个奖项可以在界面上改中奖人数。
  • 可以配置 预设名单
    • 比如某个特殊奖项,中奖人是提前定好的,只是想在现场走个流程。
  • 支持 全局排除名单(领导不参与抽奖这类场景)。

抽奖过程

  • 底部选择奖项,点击开始,3D 标签云飞快转起来。
  • 有背景音乐、开始音效,可以静音、调音量,设置会保存在浏览器里。
  • 抽完之后会展示本轮中奖人,点击可以删掉,防止误操作。

数据持久化

  • 抽奖配置、名单、结果、音频设置都存在 localStorage
  • 头像文件放在 public/user/,元数据用 IndexedDB 管。

就算现场不小心刷新了浏览器,抽过的结果还在,不至于从头来过。


为什么 UI 里专门留了一个"临时加奖项"

这里有个小故事。

去年那场年会,抽到一半,领导突然来了句:

"再加一个特别奖吧,就现在抽。"

如果奖项是写死在代码里,这种需求基本意味着:

  • 改配置;
  • 重新打包;
  • 重新部署;
  • 祈祷一切顺利。

现场几百个人盯着大屏幕,这种操作就很不现实了。

所以这次我在设计的时候,刻意做了一个分层:

  • 代码里 :有一套相对稳定的默认奖项配置,放在 src/config/lottery.ts,方便版本管理和代码 review。
  • UI 里:保留"增加奖项"的交互,可以现场临时加一个奖项、设定人数,用完就算。

实战时这个入口确实救了场:主持人说加奖,我直接在配置弹窗里加了一个新奖项,填好名字和数量,点保存,就可以继续抽了,不需要重启、不需要改代码。

这类现场工具,给业务方留一点"临时操作空间",比纯粹的"配置优雅"要重要很多。


技术堆栈简单带一下

不细讲实现,只把堆栈和大致结构列一下,方便有兴趣的同学顺着代码看:

  • 基础框架
    • Vue 3 + TypeScript
    • Vite
  • 状态管理
    • Pinia:负责当前奖项、结果列表、配置等全局状态
  • UI / 样式
    • Element Plus:弹窗、表单、按钮等基础组件
    • Tailwind CSS:布局和样式
  • 可视化 / 多媒体
    • TagCanvas:3D 标签云
    • HTML5 Audio:背景音乐和音效
  • 数据存储
    • localStorage:配置、名单、抽奖结果、音频设置
    • IndexedDB:头像元数据

目录拆法比较常规:

  • composables/:抽奖逻辑、3D 标签云封装、音频控制。
  • config/:奖项配置、用户示例数据。
  • helper/:抽奖算法、IndexedDB 操作。
  • components/:配置弹窗、结果展示、公示组件等。

如果你想改 UI 或接入自家系统,大概只需要看这几个目录就够了。


真要用在年会,大概这么搞就够了

假设你也要搞一场年会抽奖,可以按这个顺序来:

1. 拉代码 & 跑起来

bash 复制代码
pnpm install
pnpm dev

浏览器打开本地地址,先用示例数据跑一遍流程,熟悉一下界面。

2. 换成你们自己的名单

在代码里:

  1. 复制 src/config/user.example.tssrc/config/user.ts
  2. 把里面的用户列表改成你们的人:
ts 复制代码
export const user: UserItem[] = [
  { key: 1001, name: '张三' },
  { key: 1002, name: '李四' },
  // ...
];

export const excludedUsers: UserItem[] = [
  { key: 9999, name: '某总' }, // 不参与所有奖项
];

不创建 user.ts 也能跑,只是走示例数据。

3. 配奖项

  • 平时 :改 src/config/lottery.ts 里的默认奖项即可。
  • 现场 :可以直接在"抽奖配置"弹窗里:
    • 调整每个奖项的数量;
    • 给某个奖项加"预设名单"(逗号分隔的用户 ID);
    • 必要时加一个临时奖项。

4. 搞定头像(强烈建议)

视觉氛围基本靠它。

做法:

  1. 准备头像图片,建议 160×160 左右,体积别太大。
  2. 放到 public/user/ 目录。
  3. 命名规则:用户 key + 扩展名,比如 1001.jpg

项目里还有一个批量处理头像的脚本,放在 scripts/ 里,用来把原始照片统一裁成头像,细节可以看 scripts/README.md


批量头像处理脚本:从"真人照片"到统一头像

真实情况是,原始员工照片的尺寸、比例、背景色基本都不一样,直接拿来做头像效果会很怪。

为此我在 scripts/ 目录下写了一套批量头像处理脚本,主要做这些事:

  • 批量裁剪 / 缩放成统一尺寸(默认 160×160)。
  • 统一背景色、统一格式(JPG/PNG)。
  • 提供"快速模式"和"AI 增强模式",按自己的环境和需求选择。

典型用法:

  1. 把原图放到 scripts/input/
  2. 运行:
bash 复制代码
pnpm run process-images
  1. scripts/output/ 拿到处理好的头像,拷到 public/user/ 即可。

详细配置(背景色、尺寸、模式开关等)在 scripts/README.md 里都有写。


部署:用 GitHub Pages 省点事

项目现在是放在 GitHub Pages 上的,地址就是:

  • https://huyikai.github.io/AnnualRaffle/

如果你也想这么搞,一般步骤是:

  1. 把代码推到 GitHub。
  2. 用 GitHub Actions 跑一遍:
    • 安装依赖;
    • pnpm build
    • dist/ 发布到 gh-pages 分支。
  3. 在仓库 Settings → Pages 里选对应分支。

配置好之后,后面每次推代码,线上链接会自动更新。


实际使用感受

这套东西在一场不到 200 人的年会上跑了一次,整体体验还可以:

  • 3D 标签云加照片,现场观感比纯文字名单要好不少。
  • 数据持久化避免了"误刷新导致重来一次"的事故。
  • UI 里的"临时加奖项"确实发挥了作用,帮我们兜住了领导临时加奖的情况。

如果你今年也被安排做年会抽奖,可以直接拿这个项目当模板:

先跑一遍 Demo,看下是否符合你们场景,再按自己需求改名单、奖项和 UI 即可。


一点开发过程的小记

这个项目的开发过程里,我是借助了 Cursor 这类辅助工具来提效的:

  • 搭项目、改结构、重构一些逻辑时,用它来帮忙生成样板代码和做局部重构。
  • 这篇文章本身,我也借助了 Cursor 里的 AI 写作辅助,再根据自己的实际场景做了调整。

如果你平时也用 VS Code 类的编辑器,可以尝试一下类似的工作流:

让工具帮你处理重复劳动,把精力放在业务和体验上。


最后:欢迎来 GitHub 点个 Star

项目在线地址我再放一次:

  • 在线 Demo:https://huyikai.github.io/AnnualRaffle/

仓库地址在这里:

  • GitHub 仓库:https://github.com/huyikai/AnnualRaffle

如果你觉得这套年会抽奖还算顺手,或者对你有一点参考价值,欢迎顺手点个 Star。

相关推荐
天天扭码2 小时前
一文搞懂——React 19到底更新了什么
前端·react.js·前端框架
weixin_462446232 小时前
【原创】使用langchain与MCP 与 Chrome DevTools 打造可调用浏览器工具的 Chat Agent
前端·langchain·chrome devtools
OpenTiny社区2 小时前
OpenTiny 2025年度贡献者榜单正式公布~
前端·javascript·vue.js
OEC小胖胖2 小时前
08|Commit 阶段:副作用如何被组织、执行与约束
前端·react.js·前端框架·react·开源库
biubiubiu07062 小时前
Vue脚手架创建项目记录
javascript·vue.js·ecmascript
奋斗的小青年!!2 小时前
Flutter跨平台开发OpenHarmony应用:个人中心实现
开发语言·前端·flutter·harmonyos·鸿蒙
kkce2 小时前
域名CDN检测意义
服务器·前端·网络
北辰alk2 小时前
Vue 表单修饰符 .lazy:性能优化的秘密武器
vue.js
北辰alk2 小时前
`active-class`:Vue Router 链接组件的激活状态管理
vue.js