feat(game): 添加动物精灵图像变体支持并优化揭示动画

- 引入 light-animal-sprite 图像资源和 variant 属性支持
- 在动物组件中实现正常和浅色精灵图像切换功能
- 重构桌面和移动版动物组件的揭示动画逻辑
- 移除冗余的运动偏好检测和布局效果钩子
- 调整揭示动画的时间参数和平滑度曲线
- 更新资源预加载配置以包含浅色动物图像
- 修改控制组件中的投注数量调整状态管理
- 优化脚本生成精灵图像的复用性
This commit is contained in:
JiaJun
2026-06-11 15:50:22 +08:00
parent c3059549e6
commit 5768632313
48 changed files with 642 additions and 381 deletions

View File

@@ -1,53 +0,0 @@
# 36 字花前端全局代码审查报告
审查日期2026-06-05
## 审查范围
- 全局代码静态审查
- GitNexus 索引刷新与调用影响分析
- `pnpm lint`
- `pnpm build`
- 本地桌面端与移动端页面渲染检查
- DOM 文本溢出与页面横向滚动检查
## 验证结果
| 检查项 | 结果 | 备注 |
|---|---|---|
| GitNexus 索引 | 通过 | 已重新运行 `npx gitnexus analyze` |
| `pnpm lint` | 通过 | Biome 检查通过 |
| `pnpm build` | 通过 | 构建成功,但存在较大资源与 chunk |
| 桌面端页面渲染 | 通过,有样式风险 | 页面可渲染logo 与部分数字文本存在裁切/拥挤风险 |
| 移动端页面渲染 | 通过,有样式风险 | 无页面级横向滚动,部分文本高度溢出 |
| 自动化测试 | 未发现 | 仓库内未发现有效 `test/spec` 测试文件 |
## 问题清单
| 优先级 | 类型 | 位置 | 问题 | 影响 | 建议 |
|---|---|---|---|---|---|
| P0 | 安全 | `src/routes/$lang/ws-test.tsx` | `/ws-test` 测试页硬编码完整 WebSocket URL、`token``auth_token`,并且已被打进生产 bundle | 凭证泄露,任何人都可能从静态资源中提取连接信息 | 立刻删除该路由,或仅在开发环境注册;凭证全部移出前端 |
| P0 | 安全 | `.env.production`, `.env.development`, `.env.example`, `src/lib/api/api-client.ts` | `VITE_AUTH_TOKEN_SECRET` 作为前端环境变量参与签名 | `VITE_` 变量会暴露到浏览器,不能作为密钥使用 | 签名逻辑迁到服务端;前端只保存短期 token |
| P1 | 功能风险 | `src/lib/auth/auth-session.ts` | 登出或 token 失效时调用 `localStorage.clear()``sessionStorage.clear()` | 会清掉同源下非本项目或非登录相关数据GitNexus 影响面为 CRITICAL | 改成只删除明确命名的 auth/session key |
| P1 | 性能 | `src/components/app-boot-resource-gate.tsx` | 首屏前 eager 导入并预加载所有图片资源 | 大图会阻塞应用显示,移动端首屏风险高 | 只预加载首屏必要资源,其余按页面、弹窗或交互懒加载 |
| P1 | 性能 | build 输出资源 | `pc-big-reward.json` 约 3MB`pc-streak.webp` 约 3.5MB`hall-music.mp3` 约 2.5MB | 首次加载体积偏大,弱网下体验差 | 压缩动效、图片、音频;奖励弹窗和音频按需加载 |
| P2 | 性能 | `src/features/game/audio/global-audio-controller.tsx` | 初始化时为所有音频创建 `Audio` 且设置 `preload='auto'` | 即使用户未开启声音,也可能提前拉取音频 | 用户解锁或启用声音后再加载,或改为 `preload='none'` |
| P2 | 架构/体积 | `src/hooks/use-auto-hosting-runner.ts` | 自动托管 chunk gzip 后仍约 341KB疑似从 `@/api` barrel 引入过多内容 | PC/移动入口都会受影响 | 检查 `@/api` 导出,改为直接 import 具体 API拆分 runner 依赖 |
| P2 | 功能一致性 | `src/hooks/use-game-control-vm.ts`, `src/hooks/use-auto-hosting-runner.ts` | 手动下注默认所有号码共用第一项金额;自动托管按金额/玩法分组 | 当前 UI 同步筹码所以暂不一定出错,但未来混合筹码或服务端回填时容易不一致 | 明确业务约束:要么禁止混合金额,要么手动下注也按组提交 |
| P2 | 样式 | `src/style/index.css` | 全局 `design-unit` 基于 `vw`,字体随视口宽度缩放 | 移动/桌面检查发现多处数字文本高度溢出,有裁切风险 | 字体使用固定/rem + 响应式断点,不用 viewport 直接缩放正文 |
| P3 | 样式 | `src/features/game/components/desktop/desktop-animal.tsx`, `src/features/game/components/mobile/mobile-animal.tsx` | 动物格数字区域使用 `overflow-hidden``leading-none`DOM 检查有文本裁切 | 小屏或字体渲染差异下数字可能被切顶/切底 | 放宽容器高度/line-height去掉不必要的隐藏裁切 |
| P3 | 样式 | `src/features/game/components/desktop/desktop-header.tsx` | 桌面截图里 logo 文案视觉上有重叠/裁切感 | 品牌区观感不稳 | 检查 logo 素材比例和容器宽高,必要时换成未裁切素材 |
| P3 | 清理 | `src/hooks/use-topup-vm.ts` | `useTopupVm` 空实现且未被引用 | 死代码增加维护噪音 | 删除或补齐为真实 ViewModel |
| P3 | 文档 | `README.md` | README 仍是 React SPA template/empty scaffold 描述 | 项目交接信息不准确 | 更新为当前项目启动、构建、环境变量、部署说明 |
## 优先处理建议
1. 先处理 P0 安全问题:删除 `/ws-test` 生产暴露入口,移除前端密钥。
2. 再处理 P1 功能与首屏性能问题:避免全量清 storage拆掉全资源阻塞预加载。
3. 然后处理 P2 的体积、音频加载和下注模型一致性问题。
4. 最后做 P3 样式细节、死代码和文档维护。
## 备注
- 本次审查未修改业务代码。
- 仓库目前未发现有效自动化测试文件,后续修复 P0/P1 后建议补充登录态清理、下注提交、首屏渲染和资源加载相关测试。

View File

@@ -0,0 +1,198 @@
@property --gold-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes rotating-gold-border {
to {
--gold-angle: 360deg;
}
}
@keyframes gold-border-pulse {
0%,
100% {
opacity: 0.34;
transform: scale(0.992);
}
50% {
opacity: 0.54;
transform: scale(1.008);
}
}
.gold-reveal-shell {
--gold-angle: 0deg;
position: absolute;
inset: calc(var(--design-unit) * 2.6);
border-radius: calc(var(--design-unit) * 16);
overflow: hidden;
clip-path: inset(0 round calc(var(--design-unit) * 16));
pointer-events: none;
}
.gold-reveal-shell::before {
content: "";
position: absolute;
inset: 0;
border-radius: calc(var(--design-unit) * 16);
padding: calc(var(--design-unit) * 5.6);
background: conic-gradient(
from var(--gold-angle),
#534217 10%,
#534217 20%,
#ffe226 45%,
#534217 60%,
#534217 85%,
#ffe226 95%,
#534217 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 8s linear infinite;
}
.gold-reveal-shell::after {
content: "";
position: absolute;
inset: calc(var(--design-unit) * 1);
border-radius: calc(var(--design-unit) * 15);
padding: calc(var(--design-unit) * 4);
background: conic-gradient(
from calc(var(--gold-angle) + 180deg),
rgba(255, 247, 210, 0) 0%,
rgba(255, 247, 210, 0) 66%,
rgba(255, 252, 232, 0.14) 71%,
rgba(255, 254, 244, 0.98) 75%,
rgba(255, 230, 130, 0.96) 79%,
rgba(255, 247, 210, 0.18) 84%,
rgba(255, 247, 210, 0) 90%,
rgba(255, 247, 210, 0) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 2.1s linear infinite;
}
.gold-reveal-static-border {
position: absolute;
inset: calc(var(--design-unit) * 2.8);
border-radius: calc(var(--design-unit) * 16);
border: calc(var(--design-unit) * 7.4) solid rgba(181, 138, 40, 0.98);
box-shadow:
inset 0 0 calc(var(--design-unit) * 14) rgba(255, 241, 181, 0.38),
0 0 calc(var(--design-unit) * 18) rgba(255, 210, 102, 0.32);
pointer-events: none;
}
.gold-reveal-glow {
position: absolute;
inset: calc(var(--design-unit) * 1.4);
border-radius: calc(var(--design-unit) * 18);
background: radial-gradient(
circle at center,
rgba(255, 226, 92, 0.18) 0%,
rgba(219, 161, 42, 0.14) 42%,
rgba(127, 86, 13, 0.08) 62%,
transparent 82%
);
filter: blur(calc(var(--design-unit) * 2.8));
opacity: 0.36;
pointer-events: none;
animation: gold-border-pulse 1.9s ease-in-out infinite;
}
.mobile-gold-reveal-shell {
--gold-angle: 0deg;
position: absolute;
inset: calc(var(--design-unit) * 2.2);
border-radius: calc(var(--design-unit) * 7);
overflow: hidden;
clip-path: inset(0 round calc(var(--design-unit) * 7));
pointer-events: none;
}
.mobile-gold-reveal-shell::before {
content: "";
position: absolute;
inset: 0;
border-radius: calc(var(--design-unit) * 7);
padding: calc(var(--design-unit) * 2.5);
background: conic-gradient(
from var(--gold-angle),
#534217 10%,
#534217 20%,
#ffe226 45%,
#534217 60%,
#534217 85%,
#ffe226 95%,
#534217 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 8s linear infinite;
}
.mobile-gold-reveal-shell::after {
content: "";
position: absolute;
inset: calc(var(--design-unit) * 0.5);
border-radius: calc(var(--design-unit) * 6.5);
padding: calc(var(--design-unit) * 1.7);
background: conic-gradient(
from calc(var(--gold-angle) + 180deg),
rgba(255, 247, 210, 0) 0%,
rgba(255, 247, 210, 0) 66%,
rgba(255, 247, 210, 0.14) 71%,
rgba(255, 254, 244, 0.98) 75%,
rgba(255, 230, 130, 0.96) 79%,
rgba(255, 247, 210, 0.18) 84%,
rgba(255, 247, 210, 0) 90%,
rgba(255, 247, 210, 0) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 2.1s linear infinite;
}
.mobile-gold-reveal-static-border {
position: absolute;
inset: calc(var(--design-unit) * 2.7);
border-radius: calc(var(--design-unit) * 7);
border: calc(var(--design-unit) * 2.8) solid rgba(181, 138, 40, 0.98);
box-shadow:
inset 0 0 calc(var(--design-unit) * 7) rgba(255, 241, 181, 0.38),
0 0 calc(var(--design-unit) * 9) rgba(255, 210, 102, 0.32);
pointer-events: none;
}
.mobile-gold-reveal-glow {
position: absolute;
inset: calc(var(--design-unit) * 1.4);
border-radius: calc(var(--design-unit) * 8);
background: radial-gradient(
circle at center,
rgba(255, 226, 92, 0.2) 0%,
rgba(219, 161, 42, 0.16) 42%,
rgba(127, 86, 13, 0.08) 62%,
transparent 82%
);
filter: blur(calc(var(--design-unit) * 1.4));
opacity: 0.42;
pointer-events: none;
animation: gold-border-pulse 1.9s ease-in-out infinite;
}

View File

@@ -0,0 +1,268 @@
# 结算过程金色旋转边框样式与动画
本文档保存当前 PC 端与移动端在开奖/结算过程里使用的金色边框、旋转描边和发光动画,便于后续下载、复用或对接设计标注。
## 源码位置
- PC 端挂载位置:`src/features/game/components/desktop/desktop-animal.tsx`
- 移动端挂载位置:`src/features/game/components/mobile/mobile-animal.tsx`
- 样式定义位置:`src/style/index.css`
PC 端在 `revealFrame` 存在时渲染:
```tsx
<div className="gold-reveal-glow rounded-[calc(var(--design-unit)*16)]" />
<div className="gold-reveal-static-border rounded-[calc(var(--design-unit)*16)]" />
<div className="gold-reveal-shell rounded-[calc(var(--design-unit)*16)]" />
```
移动端在 `revealFrame` 存在时渲染:
```tsx
<div className="mobile-gold-reveal-glow z-10 rounded-[calc(var(--design-unit)*7)]" />
<div className="mobile-gold-reveal-static-border z-20 rounded-[calc(var(--design-unit)*7)]" />
<div className="mobile-gold-reveal-shell z-20 rounded-[calc(var(--design-unit)*7)]" />
```
## 动画机制
- `--gold-angle`:注册为 CSS angle 自定义属性,用于驱动 `conic-gradient` 旋转。
- `rotating-gold-border`:把 `--gold-angle``0deg` 推到 `360deg`,形成边框旋转。
- `gold-border-pulse`:让外层金色光晕做轻微透明度和缩放呼吸。
- `::before`主金色旋转边框8 秒一圈。
- `::after`高光扫光边框2.1 秒一圈,并从反向角度偏移 `180deg` 开始。
- `static-border`:静态金色实体边框,保证边框厚度和底色稳定。
- `glow`:外部径向金色光晕,增强结算聚焦感。
## PC 端样式
```css
@property --gold-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes rotating-gold-border {
to {
--gold-angle: 360deg;
}
}
@keyframes gold-border-pulse {
0%,
100% {
opacity: 0.34;
transform: scale(0.992);
}
50% {
opacity: 0.54;
transform: scale(1.008);
}
}
.gold-reveal-shell {
--gold-angle: 0deg;
position: absolute;
inset: calc(var(--design-unit) * 2.6);
border-radius: calc(var(--design-unit) * 16);
overflow: hidden;
clip-path: inset(0 round calc(var(--design-unit) * 16));
pointer-events: none;
}
.gold-reveal-shell::before {
content: "";
position: absolute;
inset: 0;
border-radius: calc(var(--design-unit) * 16);
padding: calc(var(--design-unit) * 5.6);
background: conic-gradient(
from var(--gold-angle),
#534217 10%,
#534217 20%,
#ffe226 45%,
#534217 60%,
#534217 85%,
#ffe226 95%,
#534217 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 8s linear infinite;
}
.gold-reveal-shell::after {
content: "";
position: absolute;
inset: calc(var(--design-unit) * 1);
border-radius: calc(var(--design-unit) * 15);
padding: calc(var(--design-unit) * 4);
background: conic-gradient(
from calc(var(--gold-angle) + 180deg),
rgba(255, 247, 210, 0) 0%,
rgba(255, 247, 210, 0) 66%,
rgba(255, 252, 232, 0.14) 71%,
rgba(255, 254, 244, 0.98) 75%,
rgba(255, 230, 130, 0.96) 79%,
rgba(255, 247, 210, 0.18) 84%,
rgba(255, 247, 210, 0) 90%,
rgba(255, 247, 210, 0) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 2.1s linear infinite;
}
.gold-reveal-static-border {
position: absolute;
inset: calc(var(--design-unit) * 2.8);
border-radius: calc(var(--design-unit) * 16);
border: calc(var(--design-unit) * 7.4) solid rgba(181, 138, 40, 0.98);
box-shadow:
inset 0 0 calc(var(--design-unit) * 14) rgba(255, 241, 181, 0.38),
0 0 calc(var(--design-unit) * 18) rgba(255, 210, 102, 0.32);
pointer-events: none;
}
.gold-reveal-glow {
position: absolute;
inset: calc(var(--design-unit) * 1.4);
border-radius: calc(var(--design-unit) * 18);
background: radial-gradient(
circle at center,
rgba(255, 226, 92, 0.18) 0%,
rgba(219, 161, 42, 0.14) 42%,
rgba(127, 86, 13, 0.08) 62%,
transparent 82%
);
filter: blur(calc(var(--design-unit) * 2.8));
opacity: 0.36;
pointer-events: none;
animation: gold-border-pulse 1.9s ease-in-out infinite;
}
```
## 移动端样式
```css
.mobile-gold-reveal-shell {
--gold-angle: 0deg;
position: absolute;
inset: calc(var(--design-unit) * 2.2);
border-radius: calc(var(--design-unit) * 7);
overflow: hidden;
clip-path: inset(0 round calc(var(--design-unit) * 7));
pointer-events: none;
}
.mobile-gold-reveal-shell::before {
content: "";
position: absolute;
inset: 0;
border-radius: calc(var(--design-unit) * 7);
padding: calc(var(--design-unit) * 2.5);
background: conic-gradient(
from var(--gold-angle),
#534217 10%,
#534217 20%,
#ffe226 45%,
#534217 60%,
#534217 85%,
#ffe226 95%,
#534217 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 8s linear infinite;
}
.mobile-gold-reveal-shell::after {
content: "";
position: absolute;
inset: calc(var(--design-unit) * 0.5);
border-radius: calc(var(--design-unit) * 6.5);
padding: calc(var(--design-unit) * 1.7);
background: conic-gradient(
from calc(var(--gold-angle) + 180deg),
rgba(255, 247, 210, 0) 0%,
rgba(255, 247, 210, 0) 66%,
rgba(255, 247, 210, 0.14) 71%,
rgba(255, 254, 244, 0.98) 75%,
rgba(255, 230, 130, 0.96) 79%,
rgba(255, 247, 210, 0.18) 84%,
rgba(255, 247, 210, 0) 90%,
rgba(255, 247, 210, 0) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: rotating-gold-border 2.1s linear infinite;
}
.mobile-gold-reveal-static-border {
position: absolute;
inset: calc(var(--design-unit) * 2.7);
border-radius: calc(var(--design-unit) * 7);
border: calc(var(--design-unit) * 2.8) solid rgba(181, 138, 40, 0.98);
box-shadow:
inset 0 0 calc(var(--design-unit) * 7) rgba(255, 241, 181, 0.38),
0 0 calc(var(--design-unit) * 9) rgba(255, 210, 102, 0.32);
pointer-events: none;
}
.mobile-gold-reveal-glow {
position: absolute;
inset: calc(var(--design-unit) * 1.4);
border-radius: calc(var(--design-unit) * 8);
background: radial-gradient(
circle at center,
rgba(255, 226, 92, 0.2) 0%,
rgba(219, 161, 42, 0.16) 42%,
rgba(127, 86, 13, 0.08) 62%,
transparent 82%
);
filter: blur(calc(var(--design-unit) * 1.4));
opacity: 0.42;
pointer-events: none;
animation: gold-border-pulse 1.9s ease-in-out infinite;
}
```
## 减少动画模式
组件中已经接入 `useReducedMotion()`。当用户系统设置减少动态效果时PC 与移动端都会把旋转和呼吸动画关闭:
```tsx
style={prefersReducedMotion ? { animation: 'none' } : undefined}
```
光晕层在减少动画模式下固定为:
```tsx
style={{
animation: 'none',
opacity: 0.36,
transform: 'scale(1)',
}}
```
## 复用注意事项
- 父容器必须是 `position: relative`,三层边框节点必须是绝对定位。
- 需要提供 `--design-unit`,当前项目用它控制 PC 与移动端等比缩放。
- `@property --gold-angle` 是旋转效果的关键;如果目标浏览器不支持 `@property`,可改用 `transform: rotate()` 方案,但需要额外包一层元素。
- 移动端额外叠加了 `animalBorderImage` 图片边框CSS 金色旋转边框位于图片边框下方,增强结算选中效果。