README (4).md
旧文件/README (4).md · 8.6 KB · 2026-09-22 15:07:00
原始文件 下载

Pool Hadiah · Vue 3 H5

ShortPro 瓜分奖池活动页(印尼语)— Vue 3 + Vite + TypeScript 实现。 100% 还原原型动画:宝箱开盖、金币爆开、彩带下落、奖池实时增长 +10 浮字、 任务进度槽位填充、Toast、中奖数字 count-up。

运行

npm install
npm run dev       # 本地开发,http://localhost:5173
npm run build     # 生产构建,输出到 dist/
npm run preview   # 预览构建产物

Node 版本建议 18+。包管理可用 npm / pnpm / yarn,任选。

接入真实后端

所有接口和实时订阅集中在 src/api/pool.ts,找 TODO 注释替换即可。

接口 用途 调用位置
GET /pool/state 初始奖池、参与人数、我的任务进度、倒计时 fetchPoolState
POST /pool/draw 真实开奖,服务端返回 5-15 Koin drawFromPool
POST /referral/invite-link 生成专属邀请链接 getInviteLink
POST /mission/claim 3/3 完成后领 10 Koin claimMissionReward
WSS /pool/live 订阅全网新用户 Lv.2 事件 subscribePoolLive
WSS /user/referral-events 订阅当前用户的下级 Lv.2 事件 subscribeMyReferrals

环境变量(新建 .env):

VITE_API_BASE=https://api.shortpro.id
VITE_WS_BASE=wss://api.shortpro.id

状态机

开奖动画由 .stage 上的三个状态类驱动:

idle       → 常态,宝箱呼吸,按钮光圈外扩
revealing  → 宝箱剧烈抖动,光芒加速旋转 0.4s 脉冲,按钮变 spinner
revealed   → 宝箱盖子翻开,28 枚金币径向飞散,50 片彩纸下落,
             中奖数字从 0 缓动到 5-15,底部 actions 滑入

状态类由 useDraw composable 管理,页面 PoolHadiahPage.vue 通过 computed 把状态映射到 <div class="stage"> 的 class 上。

每日 21:00 开放规则

useDrawEligibility composable 管理,三个阶段:

阶段 触发条件 按钮样式 按钮文案 可点击
locked-before-21 当日 21:00 前(无论有多少次数) 灰白 DIBUKA JAM 21:00 · HH:MM:SS
no-chances 21:00 后 但 当前抽奖次数 = 0 灰白 AJAK 3 TEMAN UNTUK DAPAT TARIKAN
available 21:00 后 且 抽奖次数 > 0 金色 BUKA SEKARANG · Nx

用户点到灰态按钮时,弹 toast 提示。

奖池快照与结算规则(重要)

每天 21:00 做奖池快照 → 当晚抽奖从这个冻结池子扣。

Day N:
00:00 ─────── 21:00 ─────── 24:00
└── 池子持续增长 ──┘
                ↑ 快照为 "Day N 晚池"
                └── 抽奖窗口 · 从快照扣 · 同时 21:00+ 新拉新 → 累计到 Day N+1 的晚池

为什么这么设计: 1. 公平 — 晚上所有抽奖用户从同一池子里抽,早抽晚抽没区别 2. 可预算 — 后端能准确知道每晚要发多少 3. 透明 — 用户看得到"今晚奖池:X Koin"固定数字 4. 防刷 — 防止用户"邀-抽-邀-抽"循环操作

Grand Prize 终极幸运儿大奖

UI 中的数据展示(重要)

元素 含义 更新节奏 来源
奖池主数字 今日奖池(21:00 前在涨,21:00 后从快照扣) 实时 subscribePoolLive
Koin dibagikan 历史累计已发放金额(所有天) 快速滚动 服务端 totalDistributed
Grand Prize 累计到目前的 GP 金额 跟随抽奖事件 服务端 grandPrize

任务 + 抽奖次数机制

usePoolState composable 管理,两阶段任务模型:

Phase 1(一次性,永久只能一次): - 邀请 3 个 Lv.2 好友 - 完成后自动发放 10 Koin 到钱包 - phase1Claimed 置为 true,永不回退 - 任务自动进入 Phase 2

Phase 2(无限循环,可重复): - 每邀请 3 个 Lv.2 好友 → drawChances += 1 - 任务进度 taskProgress 重置到 0,循环继续 - chancesEarnedTotal 累计历史获得次数(用于 UI "Total Nx" 展示)

抽奖机会: - 可累积,没有上限 - 21:00 前全部锁着(看得到数字,但不能抽) - 21:00 后每次开奖消耗 1 次,余 0 时按钮回到灰态 - 通过 localStorage 持久化(断网/刷新兜底,生产环境以服务端为准)

状态变更总表:

动作 变化
用户下级升 Lv.2 taskProgress++, pool += 10
Phase 1 进度到 3 phase1Claimed = true, taskProgress = 0, 10 Koin 入钱包
Phase 2 进度到 3 drawChances++, chancesEarnedTotal++, taskProgress = 0
点击开奖成功 drawChances--, pool -= prize, totalDistributed += prize, grandPrize += prize × 5%

目录

src/
├── main.ts                              入口
├── App.vue                              根组件
├── env.d.ts                             Vue + Vite 类型
├── styles/
│   ├── reset.css                        全局 reset
│   └── pool-hadiah.css                  页面所有视觉与动画(非 scoped)
├── types/index.ts                       领域类型
├── api/pool.ts                          接口桩 + 实时订阅
├── composables/
│   ├── useCountdown.ts                  通用倒计时 hh:mm:ss
│   ├── useLiveFeed.ts                   底部跑马灯
│   ├── usePoolState.ts                  奖池 + 参与人数 + 任务进度 + 浮字
│   ├── useDraw.ts                       开奖流程编排 + 状态机
│   ├── useDrawEligibility.ts            21:00 开放规则 + 每日一次
│   └── useToast.ts                      单例 Toast
└── components/
    ├── PoolHadiahPage.vue               主页面(组合所有状态,含粒子特效)
    ├── CoinSymbols.vue                  全局 SVG defs(#coin, #coinFill)
    ├── TreasureChest.vue                宝箱 SVG(含 .lid 动画组)
    └── Toast.vue                        Toast 展示层

重要工程细节

CSS 为什么不用 scoped 动画依赖 .stage.revealing .chest { ... } 这样的跨组件状态级联。 pool-hadiah.cssmain.ts 中全局引入,类名即契约,子组件按命名对应渲染。

移动端适配 vite.config.ts 里配了 postcss-px-to-viewport-8-plugin,以 375px 为设计基准, 所有 px 自动转 vw,在 iPhone SE / iPhone 15 / 平板都按比例还原。

粒子效果为什么绕过 Vue 响应式 金币(28)和彩纸(50)是一次性动画,用 Vue 响应式做 list rendering 会拖帧。 spawnCoins / spawnConfetti 直接 document.createElement 追加到模板 ref 的 DOM 节点,动画结束后节点自然被下次 reset 清掉。

防连点与失败回滚 useDraw.ts 里有两层保护: 1. status.value !== 'idle'draw() 直接 return 2. API 报错时 status.value = 'idle' 回滚,用户可以重试

低端机降级 PoolHadiahPage.vue 启动时检测 navigator.hardwareConcurrency, 核心数 ≤4 自动加 .low-end 类,隐藏 sparkle、grain、背光,关闭 backdrop-filter。 可以根据实际用户分布再细调阈值。

首屏字体 index.html 引 Google Fonts,生产环境建议换成自托管 + 字体子集化, 中文字符用不到的可以直接砍掉,仅保留 Bebas Neue、Fraunces、DM Sans 的拉丁子集, 首屏可以再省 200KB+。

埋点建议

前端接埋点 SDK 后,在以下时机上报:

pool_page_view                页面曝光(onMounted)
pool_draw_click               点"BUKA SEKARANG"
pool_draw_success             开奖成功(带 prize 字段)
pool_draw_fail                开奖失败(带 error 信息)
pool_invite_click             点"Undang"或"Undang Teman · Pool Naik"
pool_mission_complete         3/3 完成触发 claim
pool_share_copied             复制链接成功

已知可优化项

License

Proprietary · ShortPro