# 页面创建信息

## 1. 基础信息

- 页面类型：Activities
- 页面名称：Gulben Festival
- 页面目录：`src/views/Activities/GulbenFestival`
- 参考页面：未指定
- 备注：`页面创建信息.md` 是 `page.config.js` 和 `index.vue` 的生成来源；`index.vue` 先生成可预览底座，后续再按指定模块逐步补结构和接口。正式图片、接口、活动时间待补充。

## 2. 路由和权限

- route path：`/activities/gulben-festival`
- route name：`gulben-festival`
- 是否需要登录：是
- 是否加入 `src/config/security.js`：是
- 是否加入 `src/utils/permissionManager.js`：是

## 3. OSS 图片目录

- OSS 相对路径：`Activities/GulbenFestival/webp/`
- 本地开发目录：`public/oss/h5/Azizi/Activities/GulbenFestival/webp/`
- 是否需要 Codex 自动创建本地开发目录：是
- 图片是否已放入本地目录：是

### 3.1 多语言图片清单

| 图片名 | 支持语言 | 备注 |
| --- | --- | --- |
| barrageBg | en | 弹幕背景 |
| bg | en | 页面主背景 |
| cardBackBg | en | 卡牌背面背景 |
| collectGameBg | en | 集卡玩法背景 |
| draw5BtBg | en | 十连抽按钮背景 |
| draw1Bt | en | 单抽按钮 |
| GameDrawBtBg | en | 集卡玩法抽奖按钮背景 |
| gameTitleR | en | R 卡标题 |
| gameTitleSR | en | SR 卡标题 |
| gameTitleSSR | en | SSR 卡标题 |
| giftBg | en | 奖励礼物背景 |
| Gameticket | en | 任务奖励票券图 |
| helpBt | en | 帮助按钮 |
| leftBt | en | 左侧切换按钮 |
| lotteryBg | en | 抽奖区域背景 |
| lotteryPrizeActiveBg | en | 抽奖奖品选中背景 |
| lotteryPrizeBg | en | 抽奖奖品默认背景 |
| moduleActiveBg | en | 二级标签选中背景图 |
| moduleBg | en | 二级标签默认背景图 |
| RankingItem | en | 排行榜列表项背景 |
| RewardTop1 | en | Personal Rank / Reward 第一套奖励背景 |
| RewardTop2 | en | Personal Rank / Reward 第二套奖励背景 |
| RewardTop3 | en | Personal Rank / Reward 第三套奖励背景 |
| redeemRGift | en | R 礼物兑换奖励图 |
| redeemRGoat | en | R 山羊兑换奖励图 |
| redeemRLamp | en | R 灯兑换奖励图 |
| redeemRLight | en | R 灯光兑换奖励图 |
| redeemRMoon | en | R 月亮兑换奖励图 |
| redeemR2001 | en | R 卡包 2001 卡片正面图 |
| redeemR2002 | en | R 卡包 2002 卡片正面图 |
| redeemR2003 | en | R 卡包 2003 卡片正面图 |
| redeemR2004 | en | R 卡包 2004 卡片正面图 |
| redeemR2005 | en | R 卡包 2005 卡片正面图 |
| redeemSR2011 | en | SR 卡包 2011 卡片正面图 |
| redeemSR2012 | en | SR 卡包 2012 卡片正面图 |
| redeemSR2013 | en | SR 卡包 2013 卡片正面图 |
| redeemSRLamb | en | SR 羊羔兑换奖励图 |
| redeemSRScroll | en | SR 卷轴兑换奖励图 |
| redeemSRTurkey | en | SR 火鸡兑换奖励图 |
| redeemSSR2021 | en | SSR 卡包 2021 卡片正面图 |
| redeemSSR2022 | en | SSR 卡包 2022 卡片正面图 |
| redeemSSRGoat | en | SSR 山羊兑换奖励图 |
| redeemSSRLamb | en | SSR 羊羔兑换奖励图 |
| ruleActivity | en | 活动规则说明图 |
| ruleBtBg | en | 规则按钮背景 |
| ruleInfoBg | en | 规则弹窗背景 |
| tagActiveBg | en | 一级标签选中背景图 |
| tagBg | en | 一级标签默认背景图 |
| taskBtGo | en | 任务去完成按钮 |
| taskBtReceive | en | 任务领取按钮 |
| taskBtReceived | en | 任务已领取按钮 |
| taskItemBg | en | 任务列表项背景 |
| ticketHandleBtBg | en | 抽奖券操作按钮背景 |
| ticketR | en | R 抽奖券图标 |
| ticketSR | en | SR 抽奖券图标 |
| ticketSSR | en | SSR 抽奖券图标 |
| timeBg | en | 活动时间背景 |
| top1 | en | 第一名装饰 |
| top2 | en | 第二名装饰 |
| top3 | en | 第三名装饰 |
| vip1 | en | VIP 等级占位图 |
| userLevel1Bg | en | 用户等级占位背景 |
| wealthLevel1Bg | en | 财富等级占位背景 |

### 3.2 进入页面前同步预加载图片

| 图片名 | 是否包含多语言 | 备注 |
| --- | --- | --- |
| bg | 否 | 页面主背景 |
| timeBg | 否 | 活动时间背景 |
| tagActiveBg | 否 | 一级标签首屏使用 |
| tagBg | 否 | 一级标签首屏使用 |
| lotteryBg | 否 | 默认标签 CollectCards to Win Prizes 第一个 container 大背景 |
| collectGameBg | 否 | 默认标签 CollectCards to Win Prizes 第二个 container 大背景 |
| ruleBtBg | 否 | 首屏规则按钮背景 |

### 3.3 进入页面后后台预加载图片

| 图片名 | 是否包含多语言 | 备注 |
| --- | --- | --- |
| barrageBg | 否 | 弹幕区域 |
| cardBackBg | 否 | 默认标签卡牌区域 |
| draw5BtBg | 否 | 抽奖按钮 |
| draw1Bt | 否 | 抽奖按钮 |
| GameDrawBtBg | 否 | 集卡玩法抽奖按钮 |
| Gameticket | 否 | 任务奖励票券 |
| gameTitleR | 否 | 集卡标题 |
| gameTitleSR | 否 | 集卡标题 |
| gameTitleSSR | 否 | 集卡标题 |
| giftBg | 否 | 奖励内容 |
| leftBt | 否 | 切换按钮 |
| lotteryPrizeActiveBg | 否 | 抽奖奖品选中态 |
| lotteryPrizeBg | 否 | 抽奖奖品默认态 |
| moduleActiveBg | 否 | 二级标签切换时使用 |
| moduleBg | 否 | 二级标签切换时使用 |
| RankingItem | 否 | 排行榜列表项和我的排名背景 |
| RewardTop1 | 否 | Personal Rank / Reward 第一套奖励背景 |
| RewardTop2 | 否 | Personal Rank / Reward 第二套奖励背景 |
| RewardTop3 | 否 | Personal Rank / Reward 第三套奖励背景 |
| redeemRGift | 否 | 奖励兑换图 |
| redeemRGoat | 否 | 奖励兑换图 |
| redeemRLamp | 否 | 奖励兑换图 |
| redeemRLight | 否 | 奖励兑换图 |
| redeemRMoon | 否 | 奖励兑换图 |
| redeemR2001 | 否 | R 卡包 2001 卡片正面图 |
| redeemR2002 | 否 | R 卡包 2002 卡片正面图 |
| redeemR2003 | 否 | R 卡包 2003 卡片正面图 |
| redeemR2004 | 否 | R 卡包 2004 卡片正面图 |
| redeemR2005 | 否 | R 卡包 2005 卡片正面图 |
| redeemSR2011 | 否 | SR 卡包 2011 卡片正面图 |
| redeemSR2012 | 否 | SR 卡包 2012 卡片正面图 |
| redeemSR2013 | 否 | SR 卡包 2013 卡片正面图 |
| redeemSRLamb | 否 | 奖励兑换图 |
| redeemSRScroll | 否 | 奖励兑换图 |
| redeemSRTurkey | 否 | 奖励兑换图 |
| redeemSSR2021 | 否 | SSR 卡包 2021 卡片正面图 |
| redeemSSR2022 | 否 | SSR 卡包 2022 卡片正面图 |
| redeemSSRGoat | 否 | 奖励兑换图 |
| redeemSSRLamb | 否 | 奖励兑换图 |
| ruleActivity | 否 | 规则内容 |
| ruleInfoBg | 否 | 规则弹窗背景 |
| taskBtGo | 否 | 任务按钮 |
| taskBtReceive | 否 | 任务按钮 |
| taskBtReceived | 否 | 任务按钮 |
| taskItemBg | 否 | 任务列表项 |
| ticketHandleBtBg | 否 | 抽奖券操作按钮 |
| ticketR | 否 | 抽奖券图标 |
| ticketSR | 否 | 抽奖券图标 |
| ticketSSR | 否 | 抽奖券图标 |
| top1 | 否 | 排行榜前三名 |
| top2 | 否 | 排行榜前三名 |
| top3 | 否 | 排行榜前三名 |
| vip1 | 否 | 排行榜用户等级展示 |
| userLevel1Bg | 否 | 排行榜用户等级展示 |
| wealthLevel1Bg | 否 | 排行榜用户等级展示 |

## 4. 模块配置

说明：这里的配置会生成 `page.config.js` 中的 `modules`，并驱动后续 `index.vue` 初版结构。

```json
{
  "tabs": {
    "enabled": true,
    "defaultValue": "CollectCards to Win Prizes",
    "items": [
      "CollectCards to Win Prizes",
      "Personal Rank",
      "Room Rank"
    ]
  },
  "sections": [
    {
      "name": "collectCards",
      "enabled": true,
      "apis": [],
      "images": [
        "tagActiveBg",
        "tagBg"
      ],
      "remark": "一级标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
      "children": {}
    },
    {
      "name": "personalRank",
      "enabled": true,
      "apis": [],
      "images": [
        "tagActiveBg",
        "tagBg"
      ],
      "remark": "一级标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
      "children": {
        "tabs": {
          "enabled": true,
          "defaultValue": "Ranking",
          "items": [
            "Ranking",
            "Reward"
          ]
        },
        "sections": [
          {
            "name": "Reward",
            "enabled": true,
            "apis": [],
            "images": [
              "moduleActiveBg",
              "moduleBg"
            ],
            "remark": "二级奖励标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
            "children": {}
          },
          {
            "name": "Ranking",
            "enabled": true,
            "apis": [],
            "images": [
              "moduleActiveBg",
              "moduleBg"
            ],
            "remark": "二级排行榜标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
            "children": {}
          }
        ]
      }
    },
    {
      "name": "roomRank",
      "enabled": true,
      "apis": [],
      "images": [
        "tagActiveBg",
        "tagBg"
      ],
      "remark": "一级标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
      "children": {
        "tabs": {
          "enabled": true,
          "defaultValue": "Ranking",
          "items": [
            "Ranking",
            "Reward"
          ]
        },
        "sections": [
          {
            "name": "Reward",
            "enabled": true,
            "apis": [],
            "images": [
              "moduleActiveBg",
              "moduleBg"
            ],
            "remark": "二级奖励标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
            "children": {}
          },
          {
            "name": "Ranking",
            "enabled": true,
            "apis": [],
            "images": [
              "moduleActiveBg",
              "moduleBg"
            ],
            "remark": "二级排行榜标签内容区；图片为背景图，使用 itemCenter 组件居中换行展示英文标题文本，开发期间不加翻译",
            "children": {}
          }
        ]
      }
    }
  ]
}
```

## 5. 模块信息待填写

说明：下面根据第 4 节 tabs / sections 自动拆出填写坑位。你只需要补已确定的接口、字段、文案和交互规则；没填的初版页面先跳过或做静态占位。

生成说明：先生成可预览底座，只包含预加载、组件引用、图片工具、页面背景、顶部基础区域、一级/二级 tabs 容器和空内容占位。下面的业务模块和接口等你后续逐步指定后再生成。

### 5.1 全局顶部和规则入口

```json
{
  "enabled": true,
  "activityTime": "",
  "images": ["bg", "ruleBtBg", "ruleInfoBg", "ruleActivity"],
  "apis": [],
  "fields": {
    "startTime": "",
    "endTime": "2026-06-01 05:00:00"
  },
  "actions": {
    "rule": "点击规则按钮图片打开规则弹窗"
  },
  "popup": {
    "component": "MaskLayer",
    "backgroundImage": "ruleInfoBg",
    "contentComponent": "itemCenter",
    "closeRule": "点击遮罩关闭，点击弹窗内容阻止冒泡",
    "contentStyle": "padding:18vw 7vw 12vw; align-items:stretch; justify-content:flex-start; box-sizing:border-box",
    "textStyle": "max-height:62vh; overflow-y:auto; color:#FFF; font-size:1em; font-weight:600; line-height:1.35; text-align:left",
    "scrollbar": "内容区可上下滑动，但使用 rule-popup-scroll 隐藏滚动条；需要 class 处理 scrollbar-width / -ms-overflow-style / ::-webkit-scrollbar",
    "renderRule": "规则文案不要依赖源码空白或 white-space:pre-line；按标题和 (1)/(2)/(3) 拆成独立 div 行，保证 1、2、3、4 和每条小规则都单独换行",
    "content": "1、How to Earn Tickets:\n(1)Complete designated series of tasks to earnTickets.\n2、How to Use Tickets:\n(1)Each Ticket can be used to randomly drawone R-grade Card.\n3、Card Combining& Upgrade:\n(1)Combine 5 different R-grade Cards to randomlydraw one SR-grade Card.\n(2)Combine 3 different SR-grade Cards torandomly draw one SSR-grade Card.\n4、Collect & Redeem Rewards:\n(1)Collect 5 different R-grade Cards to redeem onechance to draw a Standard Reward.\n(2)Collect 3 different SR-grade Cards to redeemone chance to draw a Premium Reward.\n(3)Collect 2 different SSR-grade Cards to redeemone chance to draw a Deluxe Reward!"
  },
  "remark": "规则弹窗用 src/components/MaskLayer.vue，写法参考 src/views/Activities/LuckyDollars/Season5/index.vue。"
}
```

### 5.2 倒计时模块

```json
{
  "name": "countdown",
  "enabled": "",
  "deadline": "",
  "images": ["timeBg"],
  "fields": {
    "endTime": "2026-06-01 05:00:00",
    "remainingText": ""
  },
  "remark": "倒计时方法参考 src/views/Activities/LuckyDollars/Season5/index.vue：使用 Days / Hours / Minutes / Seconds 四个 ref，calculateCountdown 立即计算并由 startCountdown 每秒刷新；天数在模板补 D，时分秒补两位；目标时间直接在 getTargetTime 中使用 new Date(2026, 5, 1, 5, 0, 0)，不解析中文；时分秒的中间用英文冒号："
}
```

### 5.3 一级标签：CollectCards to Win Prizes

```json
{
  "sectionName": "collectCards",
  "tabText": "CollectCards to Win Prizes",
  "enabled": true,
  "apis": {
    "activityDetail": "activityDetail(activityCode)：参考 LuckyDollars Season5 的 /activity/lottery/detail/${activityCode}",
    "rewardDraw": "apiPostCardRewardDraw({ activityId:'1001', ticketType, drawCount })：POST /activity/card/reward/draw；lotteryBg 下层 draw1Bt / draw5BtBg 使用，ticketType 为 STANDARD/PREMIUM/LUXURY，对应 R/SR/SSR 奖池，drawCount 为 integer 格式的 1 或 5；请求成功后刷新 apiGetCardTicketList('1001')",
    "drawOnce": "apiPostCardDraw({ activityId:'1001', drawCount, needCoins })：POST /activity/card/draw；点击 collectGameBg 的 GameDrawBtBg 时调用，drawCount 默认为 1；当 CARD 抽卡票数量为 0 时 needCoins=true，否则 needCoins=false",
    "drawFive": "",
    "combineCard": "apiPostCardCombine({ activityId:'1001', cardTemplateIds })：POST /activity/card/combine；R 奖池需要 5 个不同 R 卡 templateId 合成 1 张 SR，SR 奖池需要 3 个不同 SR 卡 templateId 合成 1 张 SSR；SSR 奖池不展示 Combine",
    "redeemCard": "apiPostCardRedeem({ activityId:'1001', cardTemplateIds })：POST /activity/card/redeem；将当前奖池不同卡片兑换为对应奖励票，R 需要 5 个不同 R 卡、SR 需要 3 个不同 SR 卡、SSR 需要 2 个不同 SSR 卡；成功后刷新票券数量和当前卡包",
    "ticketList": "apiGetCardTicketList('1001')：GET /activity/card/ticket/list?activityId=1001",
    "rewardPrizeList": "apiGetCardRewardPrizeList('1001', poolType)：GET /activity/card/reward/prize/list?activityId=1001&poolType=${poolType}；初始化时按 STANDARD / PREMIUM / LUXURY 三个 poolType 分别调用，对应页面展示 R / SR / SSR",
    "rewardMarquee": "apiGetCardRewardMarquee('1001')：GET /activity/card/reward/marquee?activityId=1001",
    "cardBagList": "apiPostCardBagList({ activityId:'1001', cardGrade, current:1, size:20 })：POST /activity/card/bag/list；初始化时按 R / SR / SSR 三个 cardGrade 分别调用，接口虽然是 POST 但用于查询用户卡包返回数据",
    "taskList": "",
    "receiveTaskReward": "",
    "winnerHistory": ""
  },
  "images": {
    "container": ["lotteryBg", "collectGameBg", "taskModule"],
    "lottery": [ "lotteryPrizeBg", "lotteryPrizeActiveBg", "draw1Bt", "draw5BtBg"],
    "collectGame": ["cardBackBg", "GameDrawBtBg", "redeemR2001", "redeemR2002", "redeemR2003", "redeemR2004", "redeemR2005", "redeemSR2011", "redeemSR2012", "redeemSR2013", "redeemSSR2021", "redeemSSR2022"],
    "tickets": ["ticketR", "ticketSR", "ticketSSR", "ticketHandleBtBg", "helpBt"],
    "titles": ["gameTitleR", "gameTitleSR", "gameTitleSSR"],
    "tasks": ["taskItemBg", "taskBtGo", "taskBtReceive", "taskBtReceived", "Gameticket"],
    "ranking": ["RankingItem", "vip1", "userLevel1Bg", "wealthLevel1Bg"],
    "barrage": ["barrageBg"]
  },
  "fields": {
    "ticketCount": "",
    "cardBagPools": "按 cardGrade 存储 /activity/card/bag/list 返回的 body.records；R/SR/SSR 分别对应集卡奖池的卡片正面展示",
    "cardList": "",
    "lotteryPrizeList": "",
    "taskList": "",
    "barrageList": ""
  },
    "actions": {
    "rewardDraw": "点击 lotteryBg 下层 draw1Bt 调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 })，当前奖池票券数量为 0 时按钮置灰且点击无效；点击 draw5BtBg 调用 drawCount:5，当前奖池票券数量不足 5 时按钮置灰且点击无效；接口成功后刷新 apiGetCardTicketList('1001') 同步第一层三种票券数量，先根据接口结果播放 lotteryPrizeActiveBg 模拟转圈动画，动画结束后再展示 resultBg 结果弹窗，底部按钮文本为 Confirm，点击后关闭弹窗",
    "drawOnce": "点击 GameDrawBtBg 调用 apiPostCardDraw({ activityId:'1001', drawCount:1, needCoins: cardDrawTicketCount === 0 })；接口成功后刷新 apiGetCardTicketList('1001') 和 R 级 apiPostCardBagList({ activityId:'1001', cardGrade:'R', current:1, size:20 })",
    "drawFive": "",
    "combineCard": "点击 Combine 调用 apiPostCardCombine({ activityId:'1001', cardTemplateIds })；cardTemplateIds 为当前奖池用户已拥有且数量大于 0 的不同卡片 templateId 字符串数组；R 需要 5 个不同 R 卡，SR 需要 3 个不同 SR 卡；成功后刷新当前等级卡包和目标等级卡包",
    "redeemCard": "点击 Redeem 调用 apiPostCardRedeem({ activityId:'1001', cardTemplateIds })；cardTemplateIds 为当前奖池用户已拥有且数量大于 0 的不同卡片 templateId 字符串数组；R/SR/SSR 分别需要 5/3/2 个不同卡片；成功后刷新 apiGetCardTicketList('1001') 和当前等级卡包",
    "goTask": "",
    "receiveTaskReward": ""
  },
  "layout": {
    "containerRender": "container 大背景图使用 itemCenter 渲染",
    "containerStyle": "width:100vw; margin:0; padding:0",
    "wrapperStyle": "display:flex; flex-direction:column; align-items:center; gap:0; margin:0; padding:0",
    "contentStyle": "padding:0",
    "lotteryBg": {
      "component": "itemCenter",
      "contentStyle": "padding:8vw 5vw 0; align-items:stretch; justify-content:flex-start; box-sizing:border-box",
      "innerStyle": "width:100%; height:100%; display:flex; flex-direction:column; gap:0",
      "topLayer": {
        "style": "display:flex; flex:1; align-items:center; gap:4vw",
        "sideWidth": "10vw",
        "leftButton": "leftBt",
        "rightButton": "leftBt 水平翻转",
        "buttonImageStyle": "width:8vw; display:block"
      },
      "prizes": {
        "modes": {
          "lotteryR": {
            "count": 7,
            "rowCounts": [2, 3, 2],
            "ticketImage": "ticketR",
            "remark": "R 奖池当前使用 7 个测试奖品，按上层 2 个、中间 3 个、下层 2 个渲染；后续由接口返回数组替换测试数据"
          },
          "lotterySR": {
            "count": 3,
            "rowCounts": [3],
            "ticketImage": "ticketSR",
            "listClass": "lottery-prize-list-large",
            "itemClass": "lottery-prize-item-sr",
            "remark": "SR 奖池当前使用 3 个测试奖品，一行居中展示 3 个；通过缩小奖品间距和单独奖品 class 放大奖品背景图；后续由接口返回数组替换测试数据"
          },
          "lotterySSR": {
            "count": 2,
            "rowCounts": [2],
            "ticketImage": "ticketSSR",
            "listClass": "lottery-prize-list-large",
            "itemClass": "lottery-prize-item-ssr",
            "remark": "SSR 奖池当前使用 2 个测试奖品，一行居中展示 2 个；使用比 SR 更大的奖品宽度；后续由接口返回数组替换测试数据"
          }
        },
        "layout": "flex-column rows + row flex center",
        "gap": "R 使用 3vw；SR/SSR 使用 lottery-prize-list-large 将间距缩小为 1vw",
        "itemWidth": "R/SR 使用 calc((100% - var(--lottery-prize-gap) * 2) / 3)；SSR 使用 calc((100% - var(--lottery-prize-gap)) / 2.2)",
        "modeSwitch": "左右 leftBt 切换 lotteryR / lotterySR / lotterySSR，切换时重置 activeLotteryIndex；奖励池抽奖请求中暂不允许切换奖池",
        "defaultImage": "lotteryPrizeBg",
        "activeImage": "lotteryPrizeActiveBg",
        "apiData": "有 /activity/card/reward/prize/list 接口数据时优先使用接口返回数组；字段为 id、poolType、prizeName、prizeType、prizeValue、prizeImage、sortOrder；按 sortOrder 升序排序，并补充当前渲染 index 用于 lotteryPrizeActiveBg 高亮动画",
        "itemContent": "每个奖品仍使用 itemCenter，背景根据 activeLotteryIndex 切换 lotteryPrizeBg / lotteryPrizeActiveBg；itemCenter 内部使用垂直 flex，第一行 prizeImage 宽度 60%，第二行展示奖励信息；奖励信息有 days 时展示 `${days} days`，没有 days 时展示 prizeValue，不展示 prizeName",
        "remark": "奖品区域按行渲染，每一行使用 flex 居中，避免 grid 在最后一行数量不足时贴左；没有接口数据时保留本地测试奖品占位，方便预览。"
      },
      "animation": {
        "reference": "/activities/lucky-dollars-season5",
        "mode": "当前奖励池抽奖接口成功后先播放 lotteryPrizeActiveBg 模拟转圈动画，动画结束后再展示 resultBg 结果弹窗",
        "activeRule": "同一时间只有一个奖品 itemCenter 使用 lotteryPrizeActiveBg，其余使用 lotteryPrizeBg",
        "order": "从左到右、从上到下依次变化背景图",
        "resultFlow": "点击抽奖后先请求接口，接口返回中奖奖品，再根据奖品 id/name 计算最终位置，然后启动动画并停在中奖位置",
        "multiDraw": "5 连抽也直接展示结果弹窗，弹窗图片取接口返回结果中的第一个中奖奖品；没有中奖标记时取第一条结果",
        "currentPreview": "requestLotteryDraw 已接入 /activity/card/reward/draw；接口成功后先计算中奖停点并播放模拟动画，动画结束后再展示 resultBg 结果弹窗，底部按钮文本为 Confirm，点击后关闭弹窗",
        "performanceNote": "当前奖品数量少，直接切换两个 itemCenter 的 imgUrl 可以满足效果；后续如果奖品数量增加、动画频率提高或低端机出现闪烁/卡顿，优先考虑改成固定默认背景 + 单个 active 覆盖层，或用 class 切换预加载后的 CSS background，以减少图片 src 变更。",
        "decisionRule": "只要满足效果和布局，优先选择更省性能、用户体验更稳定的方案；如果 Codex 发现更好的样式或动画实现，需要先说明方案、优缺点和影响范围，并询问是否采用，不能静默替换用户已调好的结构。"
      },
        "bottomLayer": {
          "style": "display:flex; align-items:center; justify-content:space-around; gap:0; margin:0; padding:0",
          "ticketIcon": "两个抽奖按钮内部都放当前奖池 ticketImage，图片宽度 10vw；itemCenter contentStyle 使用 padding:0; align-items:flex-end; 保持水平居中并垂直贴底；点击左右切换 lotteryR / lotterySR / lotterySSR 时同步切换 ticketR / ticketSR / ticketSSR",
          "buttons": [
            {
              "image": "draw1Bt",
              "style": "width:38vw; margin:0; padding:0",
              "contentStyle": "padding:0; align-items:flex-end",
              "enabledRule": "当前奖池 poolType 对应的 STANDARD/PREMIUM/LUXURY 票券数量 >= 1 才可点击，否则添加 collect-action-disabled 置灰且点击无效",
              "action": "点击调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 })；成功后刷新 ticketList"
            },
            {
              "image": "draw5BtBg",
              "style": "width:38vw; margin:0; padding:0",
              "contentStyle": "padding:0; align-items:flex-end",
              "enabledRule": "当前奖池 poolType 对应的 STANDARD/PREMIUM/LUXURY 票券数量 >= 5 才可点击，否则添加 collect-action-disabled 置灰且点击无效",
              "action": "点击调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:5 })；成功后刷新 ticketList"
            }
          ]
        }
    },
      "collectGameBg": {
      "component": "itemCenter",
      "contentStyle": "padding:0; align-items:stretch; justify-content:flex-start; box-sizing:border-box",
      "innerStyle": "width:100%; height:100%; display:flex; flex-direction:column; gap:0; margin:0; padding:0",
      "ticketLayer": {
        "style": "height:10vw; display:flex; align-items:center; justify-content:center; gap:2vw; padding:0; margin:0",
        "items": [
          { "image": "ticketR", "ticketType": "STANDARD" },
          { "image": "ticketSR", "ticketType": "PREMIUM" },
          { "image": "ticketSSR", "ticketType": "LUXURY" }
        ],
        "itemStyle": "flex:1; min-width:0; height:100%; display:flex; align-items:center; justify-content:center; gap:1vw",
        "help": "第一个单位内贴顶放 2vw 宽度 helpBt",
        "ticketImageStyle": "width:10vw; display:block",
        "text": ":remainingCount，默认 0",
        "textStyle": "color:#fff; font-size:1em; font-weight:700"
      },
      "tipLayer": {
        "text": "*Redeem Your Cards and Draw for Amazing Rewards!",
        "style": "color:#D2D2D2; font-size:1em; font-weight:600; text-align:center; padding:0; margin:0"
      },
      "titleLayer": {
        "height": "10vw",
        "titles": {
          "gameR": "gameTitleR",
          "gameSR": "gameTitleSR",
          "gameSSR": "gameTitleSSR"
        },
        "imageStyle": "height:10vw; width:auto; display:block"
      },
      "prizeLayer": {
        "modes": {
          "gameR": 5,
          "gameSR": 3,
          "gameSSR": 2
        },
        "sideWidth": "10vw",
        "leftButton": "leftBt",
        "rightButton": "leftBt 水平翻转",
        "centerStyle": "flex:1; min-width:0; height:20vw; display:flex; align-items:center; justify-content:space-around; gap:0",
        "cardImage": "cardBackBg",
        "cardStyle": "height:100%; width:auto; display:block",
        "cardBagApi": {
          "endpoint": "POST /activity/card/bag/list",
          "params": {
            "activityId": "'1001'",
            "cardGrade": "R / SR / SSR",
            "current": 1,
            "size": 20
          },
          "responseRecords": "body.records[]；字段包含 cardTemplateId、cardGrade、cardName、cardImage、quantity",
          "imageMap": {
            "2001": "redeemR2001",
            "2002": "redeemR2002",
            "2003": "redeemR2003",
            "2004": "redeemR2004",
            "2005": "redeemR2005",
            "2011": "redeemSR2011",
            "2012": "redeemSR2012",
            "2013": "redeemSR2013",
            "2021": "redeemSSR2021",
            "2022": "redeemSSR2022"
          },
          "renderRule": "卡位固定渲染，R 固定 5 张、SR 固定 3 张、SSR 固定 2 张；接口 records 中存在对应 cardTemplateId 时，将该卡位从 cardBackBg 替换为映射的本地 redeem 图片；未返回或未映射的卡位继续展示 cardBackBg，避免接口数据数量变化导致布局跳动。",
          "quantityBadge": "除 cardBackBg 之外的卡片正面图片都使用 itemCenter 组件承载，组件 style 使用 height:100%; width:auto; aspect-ratio:159/261; flex:0 0 auto；内部绝对定位右上角展示 records.quantity，角标样式为 border-radius:33px 33px 33px 34px; background:rgba(0,0,0,0.10); backdrop-filter:blur(2px); padding:4px; color:#fff；cardBackBg 仍然使用普通 img，不展示数量角标。"
        },
        "remark": "点击左右按钮切换 gameR / gameSR / gameSSR；第三层标题、第四层卡片正反面和第五层操作层跟随当前模式。"
      },
      "actionLayer": {
        "style": "display:flex; align-items:center; justify-content:space-around; gap:0; padding:0; margin:0",
        "buttons": [
          {
            "image": "GameDrawBtBg",
            "style": "width:40vw; margin:0; padding:0",
            "contentStyle": "padding:0; flex-direction:column; gap:0.5vw; align-items:center; justify-content:center",
            "content": ["Remaining Tickets: *CARD.remainingCount", "1000/coins"],
            "action": "点击调用 /activity/card/draw，data 为 { activityId:'1001', drawCount, needCoins }，drawCount 默认为 1；当 CARD.remainingCount 为 0 时 needCoins=true，否则 needCoins=false；请求成功后调用 ticketList 刷新 CARD.remainingCount，并只刷新 R 级 cardBagList 更新当前 R 卡 quantity"
          },
          {
            "image": "ticketHandleBtBg",
            "style": "width:25vw; margin:0; padding:0",
            "contentStyle": "padding:0; align-items:center; justify-content:center",
            "content": ["Combine * 1"],
            "visibleRule": "SSR 奖池不展示 Combine",
            "enabledRule": "R 奖池用户需要拥有 5 个不同 R 卡，SR 奖池用户需要拥有 3 个不同 SR 卡；不同卡片按当前奖池固定 cardTemplateIds 中 quantity > 0 的卡片计算，不满足时添加 collect-action-disabled 样式置灰且点击无效",
            "action": "点击调用 /activity/card/combine，data 为 { activityId:'1001', cardTemplateIds }，cardTemplateIds 为字符串数组；R 合成成功后刷新 R 和 SR 卡包，SR 合成成功后刷新 SR 和 SSR 卡包"
          },
          {
            "image": "ticketHandleBtBg",
            "style": "width:25vw; margin:0; padding:0",
            "contentStyle": "padding:0; flex-direction:column; gap:0.5vw; align-items:center; justify-content:center",
            "content": ["Redeem", "当前奖池票券图(ticketR/ticketSR/ticketSSR) + *1"],
            "enabledRule": "R/SR/SSR 奖池分别需要拥有 5/3/2 个不同卡片；不同卡片按当前奖池固定 cardTemplateIds 中 quantity > 0 的卡片计算，不满足时添加 collect-action-disabled 样式置灰且点击无效",
            "action": "点击调用 /activity/card/redeem，data 为 { activityId:'1001', cardTemplateIds }，cardTemplateIds 为字符串数组；成功后立即刷新 ticketList 和当前等级 cardBagList，使第一层三种票券数量和当前卡片 quantity 同步更新"
          }
        ]
      }
      ,
      "taskModule": {
        "component": "itemCenter",
        "itemImage": "taskItemBg",
        "source": "任务接口和字段先参考 src/views/Activities/LuckyDollars/Season5/index.vue 的 taskList、ActTaskList、receiveTickets、showTarget",
        "data": "当前 Gulben 页面固定 activityId=1001，taskList 默认空数组；任务列表调用 ActTaskList('1001')，领取调用 receiveTickets({ activityId:'1001', taskCode })",
        "wrapperStyle": "width:100vw; display:flex; flex-direction:column; gap:2vw; margin:0; padding:0 5vw; box-sizing:border-box",
        "itemStyle": "width:100%; margin:0; padding:0",
        "contentStyle": "display:flex; padding:8px; justify-content:space-between; align-items:center",
        "layout": "v-for=\"task in taskList\"；左侧展示 showTarget(task) 和 Rewards:*rewardValue；右侧使用 itemCenter 展示 taskBtGo / taskBtReceive / taskBtReceived，并显示 t('task_go') / t('task_receive') / t('task_received')；不展示 Season5 中的任务类型图标和票券图标",
        "actions": "taskStatus === 1 时点击 taskBtReceive 的 itemCenter 调用 receiveTaskReward(task.taskCode)"
      }
    }
  },
  "remark": "补集卡/抽奖/任务的数据来源和按钮行为。CollectCards to Win Prizes 当前从上到下拆成 lotteryBg、collectGameBg 两个 container 大背景模块；初始不加边框虚线，padding、margin、gap 先设为 0，后续再按预览微调。"
}
```

### 5.4 一级标签：Personal Rank

```json
{
  "sectionName": "personalRank",
  "tabText": "Personal Rank",
  "enabled": true,
  "children": {
    "Reward": {
      "enabled": true,
      "apis": {
        "rewardConfig": "apiGetActivityResource：/props-activity-cnf/client/listActivityResource?sysOrigin=LIKEI&activityType=${rewardActivityType}",
        "receiveReward": ""
      },
      "images": {
        "tab": ["moduleBg", "moduleActiveBg"],
        "rewardBg": ["RewardTop1", "RewardTop2", "RewardTop3"],
        "rewardItems": ["redeemRGift", "redeemRGoat", "redeemRLamp", "redeemRLight", "redeemRMoon", "redeemSRLamb", "redeemSRScroll", "redeemSRTurkey", "redeemSSRGoat", "redeemSSRLamb"]
      },
      "fields": {
        "rewardList": "apiGetActivityResource 返回数组的前 3 套；每套 propsGroup.activityRewardProps 最多取 6 个",
        "receiveStatus": "",
        "threshold": ""
      },
      "actions": {
        "receiveReward": ""
      },
      "layout": {
        "container": "Personal Rank / Reward 标签只展示 RewardTop1、RewardTop2、RewardTop3 三个 itemCenter，宽度均为 100vw，从上往下排序",
        "source": "布局模仿 src/views/Activities/DailyRecharge/index.vue 的奖励展示模块：左侧一个主奖励，右侧网格奖励",
        "dataRule": "接口返回 10 套奖励时只取前 3 套；每套最多 6 个奖励，第 1 个作为左侧大图，后 5 个放右侧；如果某套只有 5 个奖励，则复制第 5 个奖励补到第 6 个位置用于预览铺满右侧第三行",
        "rightGrid": "右侧奖励固定分 3 行，前两行每行 2 个，第三行使用单个展示位宽度并居中；即使第三行没有奖励，也保留一个不可见占位，避免模块高度和居中位置跳动",
        "display": "奖励图使用 reward.cover，图片错误处理使用 handleRewardImageError；奖励文案统一使用 formatRewardDisplay(reward.type, reward)，规则与 DailyRecharge 保持一致，不额外优先展示 reward.content",
        "fallback": "接口未返回时临时使用 redeemRGift / redeemRGoat / redeemRLamp / redeemRLight / redeemRMoon / redeemSRLamb 组成 3 套本地预览奖励"
      },
      "remark": "补个人榜奖励配置、领取条件和领取接口；当前先展示奖励配置，不接领取。"
    },
    "Ranking": {
      "enabled": true,
      "apis": {
        "rankList": "getWeekStarRanking：/activity/week-star/ranking?type=${type}&current=${current}&size=${size}",
        "myRank": "getMyWeekStarRanking：/activity/week-star/my-rank?type=${type}&userId=${userId}",
        "userInfo": "getMemberProfile：用于获取 my-rank 需要的 userId"
      },
      "images": {
        "tab": ["moduleBg", "moduleActiveBg"],
        "top": ["top1", "top2", "top3"]
      },
      "rankConfig": {
        "loadMode": "remote",
        "type": "THIS_WEEK",
        "pageSize": 10,
        "topCount": 3,
        "fieldMap": {
          "avatar": "userAvatar",
          "name": "userNickname -> userName",
          "value": "quantity -> amount",
          "userId": "userId",
          "rank": "rank"
        }
      },
      "remark": "个人榜 Ranking 数据改用 /activity/week-star 的排行榜接口；首次进入加载第一页和我的排名，列表底部放 rankingLoadmore 哨兵，通过 IntersectionObserver 上拉分页，hasMore/loading 防止重复请求。"
    }
  }
}
```

### 5.5 一级标签：Room Rank

```json
{
  "sectionName": "roomRank",
  "tabText": "Room Rank",
  "enabled": true,
  "children": {
    "Reward": {
      "enabled": true,
      "apis": {
        "rewardConfig": "",
        "receiveReward": ""
      },
      "images": {
        "tab": ["moduleBg", "moduleActiveBg"],
        "rewardBg": ["ruleActivity"],
        "rewardItems": []
      },
      "fields": {
        "rewardList": "",
        "receiveStatus": "",
        "threshold": ""
      },
      "actions": {
        "receiveReward": ""
      },
      "layout": {
        "component": "img",
        "image": "ruleActivity",
        "style": "width:100vw; display:block",
        "remark": "Room Rank / Reward 暂不接奖励配置，直接 100vw 展示 ruleActivity"
      },
      "remark": "当前按用户要求直接展示 ruleActivity；后续如果要接房间榜奖励配置再补接口。"
    },
    "Ranking": {
      "enabled": true,
      "apis": {
        "rankList": "",
        "myRank": ""
      },
      "images": {
        "tab": ["moduleBg", "moduleActiveBg"],
        "top": ["top1", "top2", "top3"],
        "list": ["RankingItem"]
      },
      "rankConfig": {
        "loadMode": "remote",
        "pageSize": 10,
        "topCount": 3,
        "fieldMap": {
          "avatar": "userAvatar",
          "name": "userName / userNickname",
          "value": "amount / quantity",
          "roomId": "userProfile.inRoomId",
          "rank": "rank / ranking"
        }
      },
      "layout": {
        "source": "展示结构复用 Personal Rank / Ranking 的排行榜部分",
        "difference": "不展示 Personal Rank 的活动礼物 giftBg 模块；前三名头像保持 border-radius:50%，第四名以后和我的排名头像使用 border-radius:4px；第四名以后和我的排名只展示排名、头像、名称、ID、金币数量，不展示 vip1 / userLevel1Bg / wealthLevel1Bg 三个等级图片",
        "myRanking": "切换到 Room Rank / Ranking 时同样固定贴底展示我的排名，样式跟列表一致，只保留名称、ID 和金币数量",
        "pagination": "使用 GET /room/contribution/total-rank?startDate=20260518&endDate=20260524&size=20；当前接口没有 current/page 参数，加载完成后 rankingHasMore=false，不再触发上拉继续请求"
      },
      "remark": "Room Rank / Ranking 当前复用 Personal Rank 排行榜接口和布局，按房间榜展示要求裁掉礼物模块与等级图。"
    }
  }
}
```

### 5.6 接口参数

```json
{
  "activityId": "1001",
  "activityCode": "",
  "activityType": "",
  "personalRankActivityType": "THIS_WEEK",
  "roomRankActivityType": "",
  "rewardActivityType": "DAILY_RECHARGE_TASK2",
  "otherParams": {}
}
```

### 5.7 组件需求

```json
[
  {
    "name": "BackgroundLayer",
    "enabled": "",
    "remark": "页面主背景 bg"
  },
  {
    "name": "itemCenter",
    "enabled": "是",
    "remark": "一级/二级标签图片背景 + 居中文本；倒计时天/时/分/秒图片背景"
  },
  {
    "name": "ActivityTabs",
    "enabled": "",
    "remark": "一级标签和二级标签；如不用通用组件，则按图片背景自定义"
  },
  {
    "name": "ActivityLotteryGrid",
    "enabled": "",
    "remark": "CollectCards 抽奖区域"
  },
  {
    "name": "Barrage",
    "enabled": "",
    "remark": "CollectCards 弹幕区域"
  },
  {
    "name": "topUser / TopUser",
    "enabled": "",
    "remark": "个人榜/房间榜前三名展示；GulbenFestival 使用页面私有组件 src/views/Activities/GulbenFestival/components/topUser.vue，不直接修改或复用参考页面组件"
  }
]
```

### 5.8 当前 Vue 布局与样式同步记录

说明：本节按实时 `src/views/Activities/GulbenFestival/index.vue` 反向同步，用于记录当前可预览底座已经调好的结构和样式。后续如果本轮执行方案是“md -> vue”，则以本节为源同步到 Vue；如果本轮执行方案是“vue -> md”，则以实时 Vue 为准更新本节。

```json
{
  "syncSource": "src/views/Activities/GulbenFestival/index.vue",
  "lastSyncDirection": "vue -> md",
  "layout": {
    "root": {
      "class": "fullPage",
      "styleBlock": "style lang=\"scss\" scoped 顶部固定放全局文字色、fullPage、响应式基础字号 media query"
    },
    "loading": {
      "style": "display:flex; min-height:100vh; align-items:center; justify-content:center"
    },
    "pageShell": {
      "class": "scroll",
      "dir": "isRtlLayout ? 'rtl' : 'ltr'",
      "style": "min-height:100vh; position:relative; overflow-x:hidden"
    },
    "background": {
      "component": "BackgroundLayer",
      "images": ["bg"],
      "remark": "背景图内包含标题和顶部视觉信息"
    },
    "content": {
      "tag": "main",
      "style": "position:relative; z-index:1; width:min(100vw, 480px); min-height:100vh; margin:0 auto; padding:100vw 4vw 12vw",
      "remark": "顶部 padding 预留背景标题和活动视觉信息位置"
    }
  },
  "rows": [
    {
      "name": "rule",
      "order": 1,
      "wrapperClass": "rule-row",
      "rtlRule": "[dir='rtl'] .rule-row { justify-content: flex-start; }",
      "component": "itemCenter",
      "image": "ruleBtBg",
      "style": "width:15vw; flex:0 0 15vw",
      "contentStyle": "padding: 1.5vw 2vw;",
      "text": "rule",
      "textStyle": "color:#fff8d7; font-size:1em; font-weight:800; line-height:1; text-align:center; text-shadow:0 1px 2px rgba(63,20,57,.5); white-space:nowrap"
    },
    {
      "name": "countdown",
      "order": 2,
      "wrapperStyle": "display:flex; justify-content:center; margin-bottom:4vw",
      "innerStyle": "width:min(95vw, 440px); display:flex; align-items:center; justify-content:space-between; gap:1vw; padding:0 2vw; overflow:hidden",
      "itemClass": "countdown-cell",
      "itemStyle": "width:min(18vw, 88px); min-width:0; flex:0 1 min(18vw, 88px)",
      "valueClass": "countdown-value",
      "colonClass": "countdown-colon",
      "image": "timeBg",
      "contentStyle": "padding: 1.8vw 1vw;",
      "data": "参考 LuckyDollars Season5：Days / Hours / Minutes / Seconds 四个 ref；getTargetTime 直接返回 new Date(2026, 5, 1, 5, 0, 0).getTime()，月份从 0 开始，5 代表 6 月；calculateCountdown 负责计算并在过期后清零和停止定时器；startCountdown 立即执行一次并 setInterval 每秒刷新",
      "remark": "天数在模板补 D；时分秒之间使用英文冒号"
    },
    {
      "name": "topTabs",
      "order": 3,
      "wrapperStyle": "display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); align-items:stretch; gap:2vw; margin:0 0 4vw",
      "buttonClass": "image-tab top-tab",
      "activeClass": "image-tab-active",
      "activeTextSelector": ".image-tab-active span",
      "activeTextColor": "#5B0000",
      "component": "itemCenter",
      "contentStyle": "padding: 0 1vw;",
      "images": ["tagBg", "tagActiveBg"]
    },
    {
      "name": "content",
      "order": 4,
      "panelStyle": "min-height:86vw; padding:0; margin:0",
      "titleStyle": "",
      "childTabsStyle": "display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:2vw; margin-bottom:4vw",
      "containerImages": ["barrage", "lotteryBg", "collectGameBg", "taskModule"],
      "containerWrapperStyle": "display:flex; flex-direction:column; align-items:center; gap:0; margin:0; padding:0",
      "containerItemComponent": "itemCenter",
      "containerItemStyle": "width:100vw; margin:0; padding:0",
      "containerItemContentStyle": "padding:0",
      "lotteryBgLayout": {
        "contentStyle": "padding:8vw 5vw 0; align-items:stretch; justify-content:flex-start; box-sizing:border-box",
        "innerStyle": "width:100%; height:100%; display:flex; flex-direction:column; gap:0",
        "topLayer": "display:flex; flex:1; align-items:center; gap:4vw",
        "sideColumns": "左右两列 width:10vw; flex:0 0 10vw; 内部居中放 leftBt，右侧 leftBt 水平翻转；点击左右按钮切换 lotteryR / lotterySR / lotterySSR",
        "prizeGrid": "中间 flex:1；使用 flex-direction:column 分行渲染，行内 flex 居中；R 使用 --lottery-prize-gap:3vw，SR/SSR 使用 lottery-prize-list-large 缩小到 1vw；接口返回奖品按 sortOrder 排序后渲染，lotteryR 默认行数 2/3/2，lotterySR 3 个一行展示并放大，lotterySSR 2 个一行展示并更大；默认 lotteryPrizeBg，当前 activeLotteryIndex 使用 lotteryPrizeActiveBg；奖品 itemCenter 内部垂直展示 60% 宽 prizeImage 和 formatRewardDisplay 生成的奖励信息",
        "animation": "当前奖励池抽奖播放 lotteryPrizeActiveBg 模拟转圈动画；点击 1 抽或 5 连抽接口成功后先计算中奖停点并播放动画，动画结束后再展示 resultBg 结果弹窗，底部按钮文本为 Confirm，点击后关闭弹窗。",
        "performanceDecision": "当前先保留直接切换 imgUrl 的实现，因为测试奖品数量少且两张图已预加载，成本可控。若后续体验不稳，优先提议改为默认背景常驻 + 单个 active 覆盖层，或 class 切 CSS background；任何更优样式方案先询问用户是否采用。",
        "bottomLayer": "display:flex; align-items:center; justify-content:space-around; gap:0; margin:0; padding:0；draw1Bt 和 draw5BtBg 宽度 38vw；两个按钮内部水平居中、垂直贴底展示 10vw 当前奖池票券图，切换 lotteryR / lotterySR / lotterySSR 时同步切换 ticketR / ticketSR / ticketSSR；draw1Bt 需要当前奖池奖励票数量 >= 1，draw5BtBg 需要 >= 5，不满足时置灰且不给点击；点击调用 /activity/card/reward/draw，ticketType 使用当前奖池 poolType，drawCount 为 1 或 5，成功后刷新第一层三种票券数量"
      },
      "collectGameBgLayout": {
        "layers": 5,
        "ticketLayer": "高度 10vw；padding:0；display:flex 居中；gap:2vw；三个单位 flex:1；第一个单位贴顶放 2vw helpBt，并展示 10vw ticketR + STANDARD.remainingCount；后两个单位展示 ticketSR / PREMIUM.remainingCount、ticketSSR / LUXURY.remainingCount；数量默认 0",
        "tipLayer": "文本 *Redeem Your Cards and Draw for Amazing Rewards!；color:#D2D2D2；font-size:1em；font-weight:600",
        "titleLayer": "高度 10vw；按当前 gameR / gameSR / gameSSR 展示 gameTitleR / gameTitleSR / gameTitleSSR；图片 height:10vw，width:auto",
        "prizeLayer": "左中右布局；左右 width:10vw，内部居中放 leftBt，右侧水平翻转；中间 flex:1，高度 20vw；space-around 分布；卡位固定为 gameR 5 张、gameSR 3 张、gameSSR 2 张；初始化调用 apiPostCardBagList 分别查询 R/SR/SSR 卡包，records 中存在对应 cardTemplateId 时用 redeemR2001~redeemR2005、redeemSR2011~redeemSR2013、redeemSSR2021~redeemSSR2022 替换该卡位的 cardBackBg，否则继续显示 cardBackBg；除 cardBackBg 外的正面卡片使用 itemCenter，并在右上角绝对定位显示 quantity 数量角标",
        "actionLayer": "使用 display:flex; justify-content:space-around。gameR 显示 40vw GameDrawBtBg、25vw Combine、25vw Redeem；gameSR 只显示 Combine 和 Redeem，水平均匀分布；gameSSR 只显示 Redeem，居中展示。GameDrawBtBg 内展示两行文本 Remaining Tickets: *CARD.remainingCount 和 1000/coins，点击调用 apiPostCardDraw({ activityId:'1001', drawCount:1, needCoins: CARD.remainingCount === 0 })，成功后刷新 CARD 抽卡票数量和 R 级卡包数量；Combine 直接展示 Combine * 1，不展示第二行，R/SR 分别需要拥有 5/3 个不同卡片才可点击，否则置灰；Redeem 保留两行展示，第二行保留当前奖池票券图(ticketR/ticketSR/ticketSSR)并固定展示 *1，R/SR/SSR 分别需要拥有 5/3/2 个不同卡片才可点击，成功后立即刷新票券和当前卡包",
        "modeSwitch": "点击左右 leftBt 切换 gameR / gameSR / gameSSR，同时切换第三层标题、第四层卡牌数量和第五层操作层"
      },
      "personalRewardLayout": {
        "section": "Personal Rank / Reward",
        "loadRule": "切换到 Personal Rank 下的 Reward 标签时加载；调用 apiGetActivityResource(pageConfig.activity.rewardActivityType || 'DAILY_RECHARGE_TASK2')，接口返回数组只取前 3 套用于 RewardTop1 / RewardTop2 / RewardTop3",
        "container": "三个奖励模块都使用 itemCenter，图片分别为 RewardTop1、RewardTop2、RewardTop3，宽度 100vw，从上往下排序",
        "rewardLimit": "每套奖励最多取 propsGroup.activityRewardProps 前 6 个；第 1 个作为左侧大图奖励，后 5 个作为右侧小奖励；如果某套只有 5 个奖励，则复制第 5 个奖励补到第 6 个位置用于预览铺满右侧第三行",
        "layout": "模仿 DailyRecharge 奖励展示：整体左右布局，左侧大图奖励，右侧小奖励固定分三行；前两行每行 2 个，第三行 1 个展示位宽度并居中；第三行没有奖励时也保留不可见占位",
        "display": "奖励图使用 reward.cover；文案统一使用 formatRewardDisplay(reward.type, reward)，不额外优先展示 reward.content；图片错误处理用 handleRewardImageError"
      },
      "personalRankingLayout": {
        "section": "Personal Rank / Ranking",
        "loadRule": "切换到 Personal Rank 下的 Ranking 标签时加载；礼物接口参考 KingAndQueen 的 getThisWeekRewardsAndGifts，排行榜数据改用 weekly-star：getWeekStarRanking 拉列表、getMyWeekStarRanking 拉我的排名，getMemberProfile 提供 my-rank 所需 userId；列表底部使用 rankingLoadmore + IntersectionObserver 上拉分页",
        "giftModule": {
          "component": "itemCenter",
          "image": "giftBg",
          "source": "src/views/Ranking/KingAndQueen/TopList.vue 每周礼物",
          "rule": "giftBg 已预设 3 个空位，位置与参考页面一致；不再额外使用参考页中的 webpUrl('gift') 单礼物背景，只把 weeklyGifts 的 giftPhoto / giftCandy 放进三个空位",
          "contentStyle": "padding:0; align-items:center",
          "innerStyle": "width:80%; display:flex; align-items:flex-start; justify-content:space-around; margin-top:10%"
        },
        "rankingModule": {
          "source": "布局参考 /activities/lucky-dollars-season5；数据接口改用 /activity/week-star/ranking 和 /activity/week-star/my-rank",
          "top3": "前三名使用 GulbenFestival 页面私有 TopUser 组件 src/views/Activities/GulbenFestival/components/topUser.vue，不修改 LuckyDollars / GamesKing 等参考页面组件；边框图使用 Gulben 的 top1 / top2 / top3；布局为左中右一行，顺序第二名、第一名、第三名，第二/三名 flex:1; min-width:0，第一名 flex:1.25; min-width:0；贡献值样式在私有组件中模仿 src/views/Ranking/GamesKing/components/topUserDaily.vue，使用金币图标、圆角渐变底、#ffdf36 描边、内阴影和按名次区分的渐变色；排名文字和贡献值文字统一使用白色",
          "listAfterTop3": "从第四名开始渲染列表；当前使用 Gulben 的 RankingItem 图片承载列表项；排名文字和贡献值文字统一使用白色；贡献值参考 WeeklyStar 使用金币图标 + quantity/amount，不再显示 $；名称/id 的 flex-column 中新增一行等级图，vip 图片根据 rankingList 列表项 vipLevelName 按 VISCOUNT->vip1、EARL->vip2、MARQUIS->vip3、DUKEI->vip4、KING->vip5、EMPEROR->vip6 映射展示，没有 vipLevelName 时不展示；userLevel 背景图根据 charmLevel 按 1-10/11-20/21-30/31-40/41-50 对应 userLevel1Bg~userLevel5Bg，wealthLevel 背景图根据 wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg，内部居中展示对应等级文字，文字白色 font-size:1em",
          "myRanking": "切换到 Personal Rank / Ranking 时固定贴底展示我的排名；当前同样使用 RankingItem 承载；排名文字和贡献值文字统一使用白色；贡献值使用金币图标，名称下方同步展示 vip / userLevelBg / wealthLevelBg；vip 图片根据 myRanking.vipLevelName 按 VISCOUNT->vip1、EARL->vip2、MARQUIS->vip3、DUKEI->vip4、KING->vip5、EMPEROR->vip6 映射展示，没有 vipLevelName 时不展示；userLevel 背景图根据 myRanking.charmLevel 按 1-10/11-20/21-30/31-40/41-50 对应 userLevel1Bg~userLevel5Bg，wealthLevel 背景图根据 myRanking.wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg，内部居中展示对应等级文字，文字白色 font-size:1em",
          "pagination": "pageSize=10；current 从 1 开始；接口返回 rankUsers.records 追加到 ranking；当 current * rankUsers.size >= rankUsers.total 时停止上拉；rankingLoading / rankingHasMore 防止重复请求"
        },
        "data": "pageConfig.activity.personalRankActivityType 默认 THIS_WEEK；weekly-star 字段映射为 userNickname -> userName、quantity -> amount；礼物临时使用 KingAndQueen templateId=1966020557831610370"
      },
      "roomRewardLayout": {
        "section": "Room Rank / Reward",
        "image": "ruleActivity",
        "layout": "直接展示 ruleActivity 图片，width:100vw; display:block，不额外包奖励配置模块"
      },
      "roomRankingLayout": {
        "section": "Room Rank / Ranking",
        "source": "复用 Personal Rank / Ranking 的排行榜结构和 weekly-star 上拉分页逻辑",
        "difference": "不展示活动礼物 giftBg 模块；前三名仍用 GulbenFestival 私有 TopUser 组件和 top1/top2/top3，头像保持 border-radius:50%；第四名以后和我的排名使用 RankingItem，头像使用 border-radius:4px，只展示排名、头像、名称、ID、金币数量，不展示 vip1 / userLevel1Bg / wealthLevel1Bg 等级图",
        "data": "Room Rank / Ranking 固定请求 GET /room/contribution/total-rank，参数 startDate=20260518、endDate=20260524、size=20；rankList 渲染排行榜，current 渲染我的房间榜；展示账号优先取 userProfile.ownSpecialId.account，没有时取 userProfile.account；贡献值取 totalFormat；点击房间榜条目时使用 userProfile.inRoomId 调用 src/utils/appBridge.js 的 gotoRoom 方法"
      },
      "taskModuleLayout": {
        "source": "任务接口和展示字段先参考 LuckyDollars Season5 的 taskList 模块",
        "wrapperStyle": "width:100vw; display:flex; flex-direction:column; gap:2vw; margin:0; padding:0 5vw; box-sizing:border-box",
        "itemComponent": "itemCenter",
        "itemImage": "taskItemBg",
        "contentStyle": "display:flex; padding:8px; justify-content:space-between; align-items:center",
        "content": "左侧展示 showTarget(task) 和 Rewards:*rewardValue；右侧根据 task.taskStatus 使用 itemCenter 展示 taskBtGo / taskBtReceive / taskBtReceived，并写入 t('task_go') / t('task_receive') / t('task_received')；不展示 Season5 的任务类型图标和票券图标",
        "buttonStyle": "右侧按钮 itemCenter style=min-height:7.79vw；未完成/可领取文字 font-size:1em，已领取文字 font-size:0.9em，font-weight:600，white-space:nowrap",
        "data": "当前 Gulben 页面所有涉及 activityId 的接口调用都固定传 gulbenActivityId 的值，也就是字符串 '1001'；taskList 默认空数组；任务列表调用 ActTaskList('1001')，领取调用 receiveTickets({ activityId:'1001', taskCode })"
      },
      "placeholderStyle": "min-height:58vw; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0; margin:0; padding:0",
      "previewImageClass": "preview-image",
      "placeholderCopyStyle": "color:rgba(255,246,216,.86); font-size:1em; font-weight:700; text-align:center"
    }
  ],
  "styleRules": {
    "inline": "固定不变、只用一次的布局/尺寸/间距/文本展示样式优先内联，便于开发预览调试",
    "class": "复用样式、状态样式、RTL、媒体查询、伪类和复杂模块样式保留 class",
    "dynamicStyle": "不优先使用 :style 对象；状态切换优先切 class",
    "fontSize": "除基础 media query 设置字号外，页面内展示字号统一使用 1em，便于跟随响应式基准字号变化",
    "containerBackground": "模块配置中 images.container / containerImages 这类大背景图统一使用 itemCenter，宽度 100vw，从上往下 flex 排列；开发初期 gap、padding、margin 先写 0，不额外加边框虚线。",
    "proposalBeforeChange": "后续开发中，只要满足效果和布局，Codex 可以优先考虑性能和用户体验更好的样式/动画方案；但如果方案会改变当前结构、样式写法或交互表现，需要先提出方案并询问是否使用，不要直接替换。",
    "styleBlock": [
      "* { color: #fff; }",
      ".fullPage { width:100%; min-height:100vh; overflow-x:hidden; background:#0B190D; }",
      "@media screen and (max-width: 360px) { * { font-size: 10px; } }",
      "@media screen and (min-width: 360px) { * { font-size: 12px; } }",
      "@media screen and (min-width: 768px) { * { font-size: 24px; } }",
      "@media screen and (min-width: 1024px) { * { font-size: 32px; } }",
      "[dir='rtl'] .rule-row { justify-content: flex-start; }"
    ]
  }
}
```

## 6. 建页后记录

已完成：

- 创建 `src/views/Activities/GulbenFestival/index.vue`
- 创建 `src/views/Activities/GulbenFestival/page.config.js`
- 根据本文档重新同步 `page.config.js`
- 根据本文档重新生成 `index.vue` 可预览底座
- 补充倒计时配置：使用 `timeBg` 分别展示天/时/分/秒，天数补 `D`，时分秒之间使用英文冒号
- 调整一级/二级标签页按钮选中态：按钮内部文字 `.image-tab-active span` 字体颜色为 `#5B0000`
- 调整倒计时和初始化方式参考 `src/views/Activities/LuckyDollars/Season5/index.vue`：倒计时改为 `Days` / `Hours` / `Minutes` / `Seconds` 四个 `ref`，通过 `calculateCountdown()` 和 `startCountdown()` 每秒刷新；`completePreloading()` 中并行执行 `initData()` 和关键图预加载，`initData()` 负责调用 `activityDetail(activityCode)` 获取活动详情以及任务列表接口；倒计时目标时间直接在 `getTargetTime()` 中使用 `new Date(2026, 5, 1, 5, 0, 0)`，不解析中文时间
- 按实时 Vue 同步当前布局样式：内容区 `padding: 100vw 4vw 12vw`；首行使用 `ruleBtBg` 规则按钮，默认贴右、阿语贴左；固定单点样式优先内联
- 补充 `<style lang="scss" scoped>` 基础样式：`*`、`.fullPage`、响应式字号和 `[dir='rtl']` 规则
- 页面背景色调整为 `#0B190D`；CollectCards 默认内容区使用 `lotteryBg`、`collectGameBg` 两个 `itemCenter` 大背景模块自上而下展示，初始 `gap`、`padding`、`margin` 为 `0`
- 补充 `lotteryBg` 内部预览结构：上层左右 `leftBt` 切换按钮和中间当前奖池奖品背景占位，下层 `draw1Bt` / `draw5BtBg` 两个抽奖按钮
- 调整 `lotteryBg` 奖池为 `lotteryR` / `lotterySR` / `lotterySSR` 三个本地测试奖池，左右 `leftBt` 切换；R 当前 7 个奖品按 2/3/2 三行展示，SR 3 个一行展示并放大，SSR 2 个一行展示并更大，后续由接口返回数组替换测试数据
- 补充抽奖按钮内票券图：`draw1Bt` / `draw5BtBg` 内部都使用 10vw 当前奖池票券图，水平居中并垂直贴底；切换奖池时 `ticketR` / `ticketSR` / `ticketSSR` 同步切换
- 对接 `lotteryBg` 下层奖励票抽奖：`draw1Bt` 调用 `apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 })`，当前奖池 `STANDARD/PREMIUM/LUXURY` 票券数量为 0 时置灰且点击无效；`draw5BtBg` 调用 `drawCount:5`，票券数量不足 5 时置灰且点击无效；接口成功后刷新 `apiGetCardTicketList('1001')`，同步第一层三种票券数量
- 调整 `collectGameBg` 第五层操作按钮内容和显示规则：`gameR` 显示翻卡按钮 `GameDrawBtBg`、`Combine`、`Redeem`；`gameSR` 只显示 `Combine` 和 `Redeem`，保持和 R 奖池同样按钮尺寸并水平均匀分布；`gameSSR` 只显示 `Redeem` 并居中；`GameDrawBtBg` 内两行展示 `Remaining Tickets: *CARD.remainingCount` 和 `1000/coins`；`Combine` 只展示 `Combine * 1`；`Redeem` 第二行保留当前集卡奖池票券图并固定展示 `*1`，切换 `gameR/gameSR/gameSSR` 时票券图同步切换
- 对接 `GameDrawBtBg` 抽卡按钮：点击调用 `apiPostCardDraw({ activityId:'1001', drawCount:1, needCoins })`，对应 POST `/activity/card/draw`；当 `CARD` 抽卡票数量为 0 时 `needCoins=true`，否则为 `false`；请求成功后并行刷新 `apiGetCardTicketList('1001')` 和 R 级 `apiPostCardBagList({ activityId:'1001', cardGrade:'R', current:1, size:20 })`，用于更新 `Remaining Tickets` 和当前 R 级卡片 `quantity`
- 对接 `Combine` 和 `Redeem`：`Combine` 按钮改为只展示 `Combine * 1`，取消第二行票券信息；点击调用 `apiPostCardCombine({ activityId:'1001', cardTemplateIds })`，R 奖池传 5 个不同 R 卡 templateId 合成 SR，SR 奖池传 3 个不同 SR 卡 templateId 合成 SSR，SSR 不展示该按钮；`Redeem` 点击调用 `apiPostCardRedeem({ activityId:'1001', cardTemplateIds })`，R/SR/SSR 分别需要 5/3/2 个不同卡片兑换对应抽奖券；两个按钮都根据当前奖池 `quantity > 0` 的不同卡片数量判断是否可点击，不满足时添加 `collect-action-disabled` 置灰且点击无效；请求成功后及时刷新相关卡包和票券数据
- 补充规则弹窗：点击首行 `ruleBtBg` 打开 `MaskLayer`，弹窗内容用 `itemCenter` 承载 `ruleInfoBg`，点击遮罩关闭、点击弹窗内容阻止冒泡；规则文字使用 `color:#FFF; font-size:1em; font-weight:600`；内容区 `max-height:62vh` 内可上下滑动，并通过 `rule-popup-scroll` 隐藏滚动条；规则文案按标题和 `(1)/(2)/(3)` 拆成独立行渲染
- 补充集卡活动基础接口：Gulben 页面调用卡片相关接口时固定传字符串 `activityId:'1001'`，`src/api/activity.js` 中的接口方法保持通用，不写死活动 ID；页面初始化时调用 `apiGetCardTicketList('1001')` 查询用户票券数量、按 `STANDARD/PREMIUM/LUXURY` 三个 `poolType` 调用 `apiGetCardRewardPrizeList('1001', poolType)` 查询奖励池奖品列表，分别对应页面展示 `R/SR/SSR`；调用 `apiGetCardRewardMarquee('1001')` 查询中奖飘窗列表
- 补充用户卡包查询：页面初始化时调用 `apiPostCardBagList({ activityId:'1001', cardGrade, current:1, size:20 })`，按 `R/SR/SSR` 三个 `cardGrade` 分别查询并存储；集卡玩法第四层固定渲染 R=5、SR=3、SSR=2 个卡位，接口 records 中返回对应 `cardTemplateId` 时，用 `redeemR2001~redeemR2005`、`redeemSR2011~redeemSR2013`、`redeemSSR2021~redeemSSR2022` 替换默认 `cardBackBg`，没有返回的卡位继续展示 `cardBackBg`；正面卡片使用 `itemCenter` 承载，并在右上角展示 `quantity` 数量角标，`cardBackBg` 不展示角标
- 对接抽奖区奖品池展示：`/activity/card/reward/prize/list` 返回的奖品按当前奖池 `poolType` 存储，渲染时按 `sortOrder` 升序排序并补充 `index`；奖品 itemCenter 背景继续用 `lotteryPrizeBg / lotteryPrizeActiveBg` 参与动画，内部垂直展示 `prizeImage`（宽度 60%）和奖励信息；奖励信息有 `days` 时展示 `${days} days`，没有 `days` 时展示 `prizeValue`，不展示 `prizeName`
- 补充奖励池抽奖结果逻辑：`/activity/card/reward/draw` 接口成功后先计算中奖停点并播放 `lotteryPrizeActiveBg` 模拟转圈动画，动画结束后再展示 `resultBg` 结果弹窗；底部按钮文本为 `Confirm`，点击后关闭弹窗
- 补充 `collectGameBg` 五层预览结构：票券数量、提示文案、当前奖池标题、当前奖池卡牌、操作按钮；支持 `gameR` / `gameSR` / `gameSSR` 本地切换
- 补充 CollectCards 第三个任务模块 `taskModule`：任务接口和字段先参考 LuckyDollars Season5，`taskList` 默认空数组，列表项使用 `itemCenter` + `taskItemBg`，contentStyle 固定为 `display:flex; padding:8px; justify-content:space-between; align-items:center`，不引用 Season5 的任务类型图标和票券图标；右侧任务按钮也改为 `itemCenter` 背景图 + 翻译文字；Gulben 页面中任务列表和领取任务等涉及 `activityId` 的接口统一固定传 `gulbenActivityId` 的值，也就是字符串 `'1001'`
- 补充 Personal Rank / Reward 预览结构：只展示 `RewardTop1`、`RewardTop2`、`RewardTop3` 三个 100vw 的 `itemCenter` 模块，从上往下排序；奖励数据调用 `apiGetActivityResource(rewardActivityType)`，当前 `rewardActivityType` 临时使用 `DAILY_RECHARGE_TASK2` 便于预览；接口返回 10 套奖励时只取前 3 套，每套最多 6 个奖励；如果某套只有 5 个奖励，则复制第 5 个奖励补到第 6 个位置用于预览；布局参考 DailyRecharge，左侧主奖励大图，右侧小奖励固定按 2/2/1 三行展示，第三行即使没有奖励也保留单展示位不可见占位并居中；奖励文案统一使用 `formatRewardDisplay(reward.type, reward)`
- 补充 Personal Rank / Ranking 预览结构：活动礼物模块使用 `giftBg` 的 3 个预设空位直接展示 giftPhoto / giftCandy，不额外套参考页的 `gift` 单项背景；排行榜布局参考 LuckyDollars Season5，数据改用 `/activity/week-star/ranking` 和 `/activity/week-star/my-rank`；前三名使用 GulbenFestival 私有 `components/topUser.vue` + `top1/top2/top3`，布局为第二名/第一名/第三名左中右排列且第一名稍宽；贡献值参考 WeeklyStar 使用金币图标，前三名贡献值外观在私有组件内参考 GamesKing 的 `topUserDaily.vue` 圆角渐变底；排行榜排名和贡献值文字统一使用白色；第四名以后和我的排名使用 `RankingItem` 承载，名称下方新增 vip 等级图、userLevelBg、wealthLevelBg 等级行，vip 图片根据 `vipLevelName` 按 `VISCOUNT->vip1`、`EARL->vip2`、`MARQUIS->vip3`、`DUKEI->vip4`、`KING->vip5`、`EMPEROR->vip6` 映射展示，没有 `vipLevelName` 时不展示；userLevel 背景图根据 `charmLevel` 按 1-10/11-20/21-30/31-40/41-50 对应 `userLevel1Bg~userLevel5Bg`，wealthLevel 背景图根据 `wealthLevel` 按同样分段对应 `wealthLevel1Bg~wealthLevel5Bg`，内部居中展示对应等级文字，文字白色且 `font-size:1em`；切换到该标签时贴底展示我的排名，并通过底部 `rankingLoadmore` 上拉加载更多
- `Personal Rank / Reward` 和 `Personal Rank / Ranking` 的活动奖励、活动礼物都来自 `/activity/king-queen/effective?templateId=2057676469390872578`；页面内必须通过共享缓存方法读取，避免进入 Reward 和 Ranking 时重复请求同一个 effective 接口。
- 补充 Room Rank / Reward 和 Ranking：Reward 直接 100vw 展示 `ruleActivity`；Ranking 复用 Personal Rank 的排行榜展示和 weekly-star 上拉分页，但不展示活动礼物模块；前三名头像保持 `border-radius:50%`，第四名以后列表和我的排名头像使用 `border-radius:4px`，列表和我的排名只展示名称、ID、金币数量，不展示 `vip1`、`userLevel1Bg`、`wealthLevel1Bg` 三个等级图片
- 创建本地 OSS 目录 `public/oss/h5/Azizi/Activities/GulbenFestival/webp/`
- 添加路由 `/activities/gulben-festival`
- 添加安全白名单和活动页权限片段

未填写所以跳过：

- 正式活动接口
- 正式活动 ID / code / type
- 正式图片资源和多语言图片映射
- 正式活动开始时间
- 抽奖、任务、榜单、奖励等业务模块
- 正式接口对接和字段映射

待用户微调：

- 将设计图导出的图片放入本地 OSS 目录
- 在本文档中补充活动字段、预加载图片和模块接口，再同步到 `page.config.js`
- 后续按指定模块逐步生成业务结构和接口对接
- 根据正式视觉稿替换初版占位内容

构建结果：`npm run build` 通过；保留项目既有的 Vite 运行时图片引用和动态导入提示。

本地图片目录：`public/oss/h5/Azizi/Activities/GulbenFestival/webp/`

路由 path：`/activities/gulben-festival`

## 本次同步：Room Rank / Ranking 接口

- `Room Rank / Ranking` 数据源改为 GET `/room/contribution/total-rank?startDate=20260518&endDate=20260524&size=20`。
- 接口参数固定为 `startDate:'20260518'`、`endDate:'20260524'`、`size:'20'`；当前接口没有 `current/page` 参数，所以页面加载完成后设置 `rankingHasMore=false`，不再触发上拉继续请求。
- `rankList` 渲染排行榜列表，`current` 渲染我的房间榜。
- 字段映射：`rank -> rank`，`userProfile.userAvatar -> userAvatar`，`userProfile.userNickname -> userName`，`userProfile.ownSpecialId.account` 优先作为展示账号，不存在时使用 `userProfile.account`，`userProfile.inRoomId -> roomId`，`totalFormat -> amount`。
- 排行榜点击逻辑仿照 `src/views/Ranking/Overall/Ranking.vue`：Room Rank 的前三名、第四名开始的列表项点击时使用 `userProfile.inRoomId` 调用 `src/utils/appBridge.js` 中的 `gotoRoom(roomId)`；Personal Rank 的前三名、第四名开始的列表项点击时调用 `viewUserInfo(userId)`；贴底的我的排名不绑定点击事件。

## 本次同步：图片扫描与预加载

- 已重新扫描 `public/oss/h5/Azizi/Activities/GulbenFestival/webp/` 下的图片，并同步 `src/views/Activities/GulbenFestival/page.config.js` 的 `preload`。
- `blocking` 保持首屏关键图片：`bg`、`ruleBtBg`、`timeBg`、`tagActiveBg`、`tagBg`、`lotteryBg`、`collectGameBg`。
- `background` 已补齐当前目录中除首屏关键图外的全部 `.png` 图片：`barrageBg`、`cardBackBg`、`draw1Bt`、`draw5BtBg`、`GameDrawBtBg`、`Gameticket`、`gameTitleR`、`gameTitleSR`、`gameTitleSSR`、`giftBg`、`helpBt`、`leftBt`、`lotteryPrizeActiveBg`、`lotteryPrizeBg`、`moduleActiveBg`、`moduleBg`、`RankingItem`、`redeemR2001~redeemR2005`、`redeemSR2011~redeemSR2013`、`redeemSSR2021~redeemSSR2022`、`RewardTop1~RewardTop3`、`ruleActivity`、`ruleInfoBg`、`taskBtGo`、`taskBtReceive`、`taskBtReceived`、`taskItemBg`、`ticketHandleBtBg`、`ticketR`、`ticketSR`、`ticketSSR`、`top1~top3`、`vip1~vip6`、`userLevel1Bg~userLevel5Bg`、`wealthLevel1Bg~wealthLevel5Bg`。
- 已移除 `page.config.js` 预加载中不存在于本地目录的旧素材名：`redeemRGift`、`redeemRGoat`、`redeemRLamp`、`redeemRLight`、`redeemRMoon`、`redeemSRLamb`、`redeemSRScroll`、`redeemSRTurkey`、`redeemSSRGoat`、`redeemSSRLamb`。
- 页面本地 fallback 奖励图也改为真实存在的卡片图片：`redeemR2001~redeemR2005`、`redeemSR2011`；每周礼物占位图改为 `redeemR2001`、`redeemSR2011`、`redeemSSR2021`。

## 本次同步：APP 初始化连接

- 页面初始化数据要仿照 `src/views/Activities/LuckyDollars/Season5/index.vue` 使用 `import { connectToApp } from '@/utils/appConnector.js'`。
- `onMounted` 中不直接调用 `completePreloading()`，改为调用 `connectToAppHandler()`。
- `connectToAppHandler()` 内通过 `connectToApp(() => { completePreloading() })` 在 APP 连接成功或浏览器环境初始化完成后再执行 `completePreloading()`；因此 `initData()`、首屏 blocking 图片预加载、后台图片预加载都在 APP header / 用户信息处理后触发。
- Room Rank 的 UI 仍复用当前排行榜结构，不展示活动礼物模块；前三名头像保持 `border-radius:50%`，第四名以后列表和我的排名头像保持 `border-radius:4px`，列表和我的排名只展示名称、ID、金币数量，不展示 `vip1`、`userLevel1Bg`、`wealthLevel1Bg`。

## 本次同步：奖励池抽奖结果弹窗

- `/activity/card/reward/draw` 返回数据按 `body.rewards` 解析；兼容旧的 `body.results` 和直接数组返回，最终统一存入 `drawResult.results`。
- 单抽和 5 连抽接口成功后先播放奖励池模拟转圈动画，动画结束后再展示 `resultBg` 结果弹窗。
- 结果弹窗图片优先取 `rewards` 中第一个 `isWin === true` 的奖品；如果没有中奖标记，则退回使用第一条结果；奖励池抽奖结果图宽度为 `45vw`，抽卡/合成结果图继续使用 `28vw`。
- 奖励池抽奖结果弹窗在奖品和底部按钮之间保留一行居中信息：有 `days` 时展示 `reward_days` 翻译文本，没有 `days` 时直接展示 `prizeValue`；5 连抽时 `1/5`、`2/5` 等页码在同一行内绝对定位贴右展示。
- 结果弹窗底部按钮文本为 `Confirm`，点击后关闭弹窗。
- 停点匹配优先级为 `prizeId -> prizeImage -> prizeType + prizeValue`，匹配当前奖池 `activeLotteryPrizes` 中对应奖品后，将 `lotteryPrizeActiveBg` 停在该奖品位置。
- 匹配不到接口奖品时才使用本地 `prizeIndex` 兜底；没有任何可用停点时停在当前奖池第一个奖品，避免继续使用旧测试逻辑固定停第 4 个。

## 本次同步：抽奖区奖励文案

- `lotteryBg` 奖品 itemCenter 内第二行奖励信息不展示接口返回的 `prizeName`。
- 该区域直接按接口字段展示：有 `days` 时展示 `${days} days`，没有 `days` 时展示 `prizeValue`。

## 本次同步：抽奖动画防抖动

- `lotteryBg` 奖品格子的 `itemCenter` 背景固定使用 `lotteryPrizeBg`，抽奖动画不再通过切换 `:imgUrl` 改变背景图。
- `lotteryPrizeActiveBg` 改为放在奖品格子内部的绝对定位覆盖图，通过 `lottery-prize-active-bg-show` 切换 `opacity` 展示选中态。
- 由于 `lotteryPrizeActiveBg` 和 `lotteryPrizeBg` 只有颜色差异，使用覆盖图透明度切换可以避免动画过程中反复改图片 `src`，减少移动端图片重新解码、布局重算导致的页面抖动。
- 奖品图片和奖励信息统一放到覆盖图上层，使用 `position:relative; z-index:1` 保持显示层级。

## 本次同步：抽奖高亮防抖动方案

- `lotteryBg` 奖品格子的 `itemCenter` 底图固定使用 `lotteryPrizeBg`，抽奖动画时不再通过切换 `:imgUrl` 或额外 active 图片节点来切换背景。
- 当前选中奖品只切换 `lottery-prize-item-active` class；高亮效果由 `.lottery-prize-item::after` 的叠色和阴影实现，动画中只改变 `opacity`，减少图片 `src` 变化、图片解码和布局重算导致的页面抖动。
- 因 `lotteryPrizeActiveBg` 与 `lotteryPrizeBg` 实际只有颜色差异，当前优先使用 CSS 叠色模拟 active 态；如果后续视觉要求完全一致，再微调叠色颜色、透明度和阴影参数。

## 本次同步：抽奖动画防抖动

- `lotteryBg` 奖品格背景不再通过 `:imgUrl="lotteryPrizeImage(prize.index)"` 在 `lotteryPrizeBg` 和 `lotteryPrizeActiveBg` 之间频繁切换。
- 奖品格 `itemCenter` 固定使用 `imageUrl('lotteryPrizeBg')`，选中态只切换 `lottery-prize-item-active` class。
- 高亮效果通过 `.lottery-prize-item::after` 覆盖层模拟，避免动画过程中反复改图片 `src` 导致重新解码、重排或视觉抖动。
- 奖品格增加 `overflow:hidden`、`contain: layout paint`、`isolation:isolate`、`transform:translateZ(0)`，将高亮绘制限制在单个奖品格内部，减少对页面其它区域的影响。
- 因 `lotteryPrizeActiveBg` 和 `lotteryPrizeBg` 主要只有颜色差异，当前优先使用覆盖层方案；如果后续选中态图片出现复杂纹理差异，再考虑单独预加载 active 图并使用固定覆盖层图片，而不是切换 `itemCenter` 的 `imgUrl`。

## 本次同步：抽奖选中态图片保留

- `lotteryPrizeActiveBg` 仍然用于抽奖选中态视觉，但不再作为 `itemCenter` 的 `imgUrl` 频繁切换。
- 奖品格 `itemCenter` 底图固定为 `imageUrl('lotteryPrizeBg')`。
- 每个奖品格内部常驻一张绝对定位的 `lotteryPrizeActiveBg` 覆盖图，class 为 `lottery-prize-active-bg`。
- 当前选中奖品只切换 `lottery-prize-active-bg-show`，通过 `opacity` 展示 active 图；奖品图片和奖励信息使用 `z-index:1` 保持在 active 图上层。
- 这样既保留 `lotteryPrizeActiveBg` 的设计效果，也避免抽奖动画中反复修改 `itemCenter` 背景图 `src` 导致页面抖动。

## 本次同步：抽奖默认选中态

- `activeLotteryIndex` 默认值保持 `0`，页面初始时第一个奖品位置展示 `lotteryPrizeActiveBg`。
- 切换 `lotteryBg` 的 R/SR/SSR 奖池后，也将 `activeLotteryIndex` 重置为 `0`，默认展示当前奖池第一个位置的 active 图。

## 本次同步：Personal Rank / Room Rank 二级标签顺序

- `Personal Rank` 和 `Room Rank` 下的二级标签顺序调整为 `Ranking`、`Reward`。
- 两个一级标签进入时默认打开 `Ranking` 小标签页。
- Vue 渲染二级标签时按 section `name` 匹配内容区，不再依赖 `items` 和 `sections` 的数组下标，避免后续只调整按钮顺序时内容错位。
- 点击抽奖后动画从当前 `activeLotteryIndex` 位置继续向后滚动，最终停留在接口返回中奖奖品对应的位置；下一次抽奖继续从上一次停留位置开始模拟转圈。

## 本次同步：抽奖 active 图唯一渲染

- `lotteryPrizeActiveBg` 不再每个奖品格都常驻后靠 `opacity` 隐藏。
- 当前改为只在 `prize.index === activeLotteryIndex` 的奖品格内通过 `v-if` 渲染 `lotteryPrizeActiveBg`。
- 因此同一时间 DOM 中只有当前选中格存在 active 图，避免出现所有奖品格都显示 `lotteryPrizeActiveBg` 的情况。
- `itemCenter` 的底图仍固定为 `lotteryPrizeBg`，抽奖动画仍只更新 `activeLotteryIndex`，最终停留在接口返回的中奖位置。

## 本次同步：抽奖 active 图展示响应

- `lotteryPrizeActiveBg` 从 `v-if` 改为 `v-show` 控制显示，避免抽奖动画每一步都创建和销毁 active 图片节点。
- active 图提前渲染在每个奖品格内，动画时只切换 `display`，减少 `v-smart-img`、图片挂载和解码带来的显示延迟。
- 抽奖动画速度从约 `70ms~260ms` 调整为约 `130ms~350ms`，让 `lotteryPrizeActiveBg` 的逐格变化更容易被看到。

## 本次同步：标签页切换防闪烁

- 一级标签按钮不再通过 `:imgUrl="tabButtonImage(tab.value)"` 在 `tagBg` 和 `tagActiveBg` 之间切换背景图。
- 一级标签 `itemCenter` 底图固定为 `imageUrl('tagBg')`，选中态 `tagActiveBg` 作为内部 `tab-active-bg` 覆盖图，通过 `v-show="activeTopTab === tab.value"` 控制显示。
- 二级标签按钮不再通过 `:imgUrl="childTabButtonImage(tab.value)"` 在 `moduleBg` 和 `moduleActiveBg` 之间切换背景图。
- 二级标签 `itemCenter` 底图固定为 `imageUrl('moduleBg')`，选中态 `moduleActiveBg` 作为内部覆盖图，通过 `v-show="activeChildTab === tab.value"` 控制显示。
- `.tab-background` 增加 `overflow:hidden`、`contain: layout paint`、`isolation:isolate`、`transform:translateZ(0)`，减少切换选中态时对页面其它区域的重绘影响。

## 本次同步：标签页内容区防闪烁

- 标签页内容区闪烁主要来自内容模块重新挂载后，`v-smart-img` 会先将图片透明度置为 `0`，等图片处理完成再显示。
- `src/directives/smartImage.js` 中补充 `noFade` 的保持可见逻辑：当使用 `v-smart-img.noFade` 时，加载准备阶段不再先隐藏图片，而是保持 `opacity:1` 且取消淡入过渡。
- `src/components/itemCenter.vue` 的背景图改为 `v-smart-img.noFade`，减少标签内容区大背景重新挂载时的空白闪烁。
- Gulben 页面内直接使用的 `v-smart-img` 统一改为 `v-smart-img.noFade`，私有 `components/topUser.vue` 中的边框图和金币图也改为 `noFade`。
- `page.config.js` 的 `preload.blocking` 补充标签内容区关键大图：`moduleActiveBg/moduleBg`、`taskItemBg`、`RewardTop1~3`、`ruleActivity`、`giftBg`、`RankingItem`、`top1~3`，尽量在进入页面前完成核心标签页素材加载。
