我自己构建了Scrum Poker,因为其他的都很糟糕 🎴
Source: Dev.to
(请提供您想要翻译的具体文本内容,我将为您翻译成简体中文并保持原有的格式、Markdown 语法以及技术术语不变。)
问题 😩
Sprint 计划。你的团队需要对任务进行估算。有人分享了一个 Planning Poker 链接。你点击进去。你等着。半数团队成员连不上。另一半已经去喝咖啡了。
听起来熟悉吗?
在 AGG TEAM 我们尝试了所有办法:
- Planning Poker Online – 卡顿、广告、古老的 UI
- Scrum Poker for Jira – 昂贵,需要 Jira
- PlanITpoker – 能用,但缺少 我们 的功能
在第三次估算中途崩溃后,我想:“算了,我自己来搞一个。”
真正的挑战:6个团队,1个房间 🤯
我们没有一个 Scrum 团队——我们有 六个部门:
- 前端
- 后端
- DevOps
- QA
- 分析
- 管理
每个人都想在同一个房间 同时 进行 Planning Poker。这简直是混乱——就像在同一张桌子上玩六种不同的纸牌游戏。现有工具只会说 “这里只有一个房间,自己想办法!”——这并不理想。
我构建的功能
🎰 多表支持(最多 6 张!)
核心概念:一个会话,多个相互独立的表。
interface Table {
id: string;
name: string; // "Frontend Squad", "Backend Ninjas"
revealed: boolean; // Are cards revealed for this table?
}
interface Player {
id: string;
name: string;
tableId: string; // Which table they're at
vote: string | null; // "5", "13", "?", "☕"
}- 主持人可以创建带自定义名称的表。
- 每个人选择自己的表。
- 每张表独立投票。
- 你可以看到所有表 以及 一个整体平均值。
主持人权限: 随时添加、删除、重命名表;必要时在表之间移动成员。
⚡ 实时同步
使用 Supabase 作为后端,采用简单的轮询方式:
useEffect(() => {
if (view === 'room' && roomId) {
fetchRoomState();
const interval = setInterval(fetchRoomState, 2000);
return () => clearInterval(interval);
}
}, [view, roomId]);WebSocket 会更酷,但对原型和约 50 人的场景轮询已经足够。如果用户量增长,我会改用 WebSocket。
实时更新:
- 有人加入 → 立即显示
- 有人投票 → 立刻更新
- 卡片翻开 → 所有人看到结果
- 发送表情 → 在屏幕上飞过
💓 智能断线检测
其他工具的常见问题:用户关闭标签页后,头像仍然保留(幽灵用户)。
我的解决方案使用 心跳 + 显式断开:
// Send heartbeat every 10 seconds
const sendHeartbeat = async () => {
await fetch(`${API_URL}/rooms/${roomId}/heartbeat`, {
method: 'POST',
body: JSON.stringify({ playerId }),
});
};
// Explicit disconnect on page close
window.addEventListener('beforeunload', () => {
navigator.sendBeacon(
`${API_URL}/rooms/${roomId}/disconnect`,
JSON.stringify({ playerId })
);
});结果
- 页面打开但闲置 → 只要不关闭就保持(不会被踢)
- 关闭页面/标签页 → 立即断开(≈ 2 秒)
不再有幽灵用户,房间保持干净。
🎉 表情攻击!
点击任意参与者,投掷表情。表情会 真的从你的头像飞向对方,并伴随流畅动画。
可用表情:
👍 👏 🎉 ❤️ 🚀 🔥 😂 🤔 💯 ✨ 👌 🙌
使用场景
- 👍 同意估算
- 🔥 某人提出了绝佳观点
- 😂 初级成员把一个简单任务估算成 89 点
- ☕ 明显是喝咖啡时间
这让我们的计划会 变得有趣——大家现在真的期待估算会议了!
🃏 斐波那契 + 特殊卡
经典序列:0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
加上特殊卡:
- ”?” – “我一点也不知道”
- ”☕” – “需要咖啡才能思考”
你可以 在翻牌后仍然更改投票(大多数工具会锁定投票)。讨论过程中如果发现 13 实际上应该是 8——我的工具允许这样操作。
🧮 自动平均值计算
每张表都会显示其平均值。所有表的总体平均 会在底部统一展示。
const calculateAverage = (players: Player[], revealed: boolean) => {
if (!revealed) return null;
const numericVotes = players
.map(p => p.vote)
.filter(v => v && !isNaN(Number(v)))
.map(Number);
if (numericVotes.length === 0) return null;
return (numericVotes.reduce((a, b) => a + b) / numericVotes.length).toFixed(1);
};完美回答 “所有团队的整体估算是多少?” 这一问题。
🌙 暗黑模式 + 🌍 多语言
右下角的切换按钮可以切换主题(浅色/暗色) 以及 语言(EN/RU)。设置会保存到 localStorage,下次访问时自动生效。
因为如果你的应用在 2026 年还没有暗黑模式……你到底在干什么?
技术栈 🛠
前端
- React + TypeScript
- Tailwind CSS v4
- Lucide 图标
后端
- Supabase Edge Functions (Hono + Deno)
- Supabase KV 存储(键值表)
为什么选择 Supabase?
- 快速搭建 — 无需服务器部署
- Edge Functions 让我可以编写 TypeScript 后端逻辑
- 简单的 KV 存储用于房间状态
- 免费层已足以满足原型需求
挑战与解决方案 💡
幽灵用户
- 问题:用户关闭标签页,头像仍然残留。
- 解决方案:心跳 + 使用
navigator.sendBeacon显式断开连接。
Z‑Index 争夺战
- 问题:主题切换覆盖了模态框。
- 解决方案:定义清晰的层级结构(按钮使用
z-30,模态框使用z-40)。
状态管理
- 问题:保持 6 张表格 + 玩家同步。
- 解决方案:后端单一数据源;轮询获取更新。
规划的改变
之前
- “等等,大家都加载好了吗?”
- “刷新一下,我没看到你的投票。”
- “还有谁在?”
- 尴尬的等待
之后
- 创建房间(≈ 5 秒)
- 所有人加入(瞬间)
- 投票 → 揭示 → 讨论
- 发送表情包玩乐
- 按时结束
真实反馈
“等等,计划扑克也能这么顺畅?”
“我喜欢向大家投掷火焰表情!”
“终于有个不会让我愤怒退出的工具了。”
接下来是什么? 🚀
- 会话历史(谁投了什么)
- 可选投票计时器
- 可导出报告(CSV/JSON)
- 更多语言支持
为六支团队、一个房间以及大量咖啡而构建。
# Countdown
- Custom decks (T‑shirt sizes?)
- Sound effects (optional)
- Jira integration
---关键要点 🧠
1. 构建你需要的东西
别再等 完美 的工具了。80 % 的功能在一个周末完成,胜过 100 % 的功能迟迟“最终实现”。
2. 轮询并非邪恶
WebSocket 很酷,但轮询对小团队来说非常好用。不要过度设计。
3. 小的惊喜很重要
表情符号功能只用了 2 小时,却成了大家的最爱。细小的东西能带来巨大的差异。
4. 暗色模式是必需的
说真的。已经 2026 年了。
试一试! 🎮
AGG POKER – 让团队通过交互式 Scrum 估算扑克工具进行协作估算,用户可以加入会话,简化项目规划。
免费使用
结论
有时只需几个晚上的编码,就能省去数月的挫折感。而且还能学到新东西,双赢。
如果你的团队也有相同的计划扑克痛点——自己动手构建吧!我们现在拥有出色的框架、免费托管以及 AI 助手,毫无借口。
有疑问?想法?想贡献? 留下评论吧!
附言 是的,企业级解决方案是存在的。我的方案成本为 $0,运行完美,且构建过程很有趣。 😄