aslan-h5/src/views/Activities/GulbenFestival/页面创建信息.md
2026-05-23 18:49:13 +08:00

77 KiB
Raw Blame History

页面创建信息

1. 基础信息

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

2. 路由和权限

  • route path/activities/gulben-festival
  • route namegulben-festival
  • 是否需要登录:是
  • 是否加入 src/config/security.js:是
  • 是否加入 src/utils/permissionManager.js:是

3. OSS 图片目录

  • OSS 相对路径:Activities/GulbenFestival/
  • 本地开发目录:public/oss/h5/Azizi/Activities/GulbenFestival/
  • 是否需要 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 初版结构。

{
  "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": "Reward",
          "items": [
            "Reward",
            "Ranking"
          ]
        },
        "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": "Reward",
          "items": [
            "Reward",
            "Ranking"
          ]
        },
        "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 全局顶部和规则入口

{
  "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 倒计时模块

{
  "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 四个 refcalculateCountdown 立即计算并由 startCountdown 每秒刷新;天数在模板补 D时分秒补两位目标时间直接在 getTargetTime 中使用 new Date(2026, 5, 1, 5, 0, 0),不解析中文;时分秒的中间用英文冒号:"
}

5.3 一级标签CollectCards to Win Prizes

{
  "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/drawlotteryBg 下层 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/combineR 奖池需要 5 个不同 R 卡 templateId 合成 1 张 SRSR 奖池需要 3 个不同 SR 卡 templateId 合成 1 张 SSRSSR 奖池不展示 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.recordsR/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 使用 3vwSR/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 / lotteryPrizeActiveBgitemCenter 内部使用垂直 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图片宽度 10vwitemCenter 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:#fffcardBackBg 仍然使用普通 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/drawdata 为 { 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/combinedata 为 { 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/redeemdata 为 { 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任务列表调用 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

{
  "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

{
  "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: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 接口参数

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

5.7 组件需求

[
  {
    "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 为准更新本节。

{
  "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 Season5Days / Hours / Minutes / Seconds 四个 refgetTargetTime 直接返回 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": ["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:3vwSR/SSR 使用 lottery-prize-list-large 缩小到 1vw接口返回奖品按 sortOrder 排序后渲染lotteryR 默认行数 2/3/2lotterySR 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:0draw1Bt 和 draw5BtBg 宽度 38vw两个按钮内部水平居中、垂直贴底展示 10vw 当前奖池票券图,切换 lotteryR / lotterySR / lotterySSR 时同步切换 ticketR / ticketSR / ticketSSRdraw1Bt 需要当前奖池奖励票数量 >= 1draw5BtBg 需要 >= 5不满足时置灰且不给点击点击调用 /activity/card/reward/drawticketType 使用当前奖池 poolTypedrawCount 为 1 或 5成功后刷新第一层三种票券数量"
      },
      "collectGameBgLayout": {
        "layers": 5,
        "ticketLayer": "高度 10vwpadding:0display: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:#D2D2D2font-size:1emfont-weight:600",
        "titleLayer": "高度 10vw按当前 gameR / gameSR / gameSSR 展示 gameTitleR / gameTitleSR / gameTitleSSR图片 height:10vwwidth:auto",
        "prizeLayer": "左中右布局;左右 width:10vw内部居中放 leftBt右侧水平翻转中间 flex:1高度 20vwspace-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 RedeemgameSR 只显示 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)并固定展示 *1R/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-stargetWeekStarRanking 拉列表、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~userLevel5BgwealthLevel 背景图根据 wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg内部居中展示对应等级文字文字白色 font-size:1em",
          "myRanking": "切换到 Personal Rank / Ranking 时固定贴底展示我的排名;当前同样使用 RankingItem 承载;排名文字和贡献值文字统一使用白色;贡献值使用金币图标,名称下方同步展示 vip / userLevelBg / wealthLevelBgvip 图片根据 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~userLevel5BgwealthLevel 背景图根据 myRanking.wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg内部居中展示对应等级文字文字白色 font-size:1em",
          "pagination": "pageSize=10current 从 1 开始;接口返回 rankUsers.records 追加到 ranking当 current * rankUsers.size >= rankUsers.total 时停止上拉rankingLoading / rankingHasMore 防止重复请求"
        },
        "data": "pageConfig.activity.personalRankActivityType 默认 THIS_WEEKweekly-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 改为 4px第四名以后和我的排名使用 RankingItem只展示排名、头像、名称、ID、金币数量不展示 vip1 / userLevel1Bg / wealthLevel1Bg 等级图",
        "data": "Room Rank / Ranking 固定请求 GET /room/contribution/total-rank参数 startDate=20260518、endDate=20260524、size=20rankList 渲染排行榜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.9emfont-weight:600white-space:nowrap",
        "data": "当前 Gulben 页面所有涉及 activityId 的接口调用都固定传 gulbenActivityId 的值,也就是字符串 '1001';任务列表调用 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'] 规则
  • 页面背景色调整为 #0B190DCollectCards 默认内容区使用 lotteryBgcollectGameBg 两个 itemCenter 大背景模块自上而下展示,初始 gappaddingmargin0
  • 补充 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 显示翻卡按钮 GameDrawBtBgCombineRedeemgameSR 只显示 CombineRedeem,保持和 R 奖池同样按钮尺寸并水平均匀分布;gameSSR 只显示 Redeem 并居中;GameDrawBtBg 内两行展示 Remaining Tickets: *CARD.remainingCount1000/coinsCombine 只展示 Combine * 1Redeem 第二行保留当前集卡奖池票券图并固定展示 *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
  • 对接 CombineRedeemCombine 按钮改为只展示 Combine * 1,取消第二行票券信息;点击调用 apiPostCardCombine({ activityId:'1001', cardTemplateIds })R 奖池传 5 个不同 R 卡 templateId 合成 SRSR 奖池传 3 个不同 SR 卡 templateId 合成 SSRSSR 不展示该按钮;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~redeemR2005redeemSR2011~redeemSR2013redeemSSR2021~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列表项使用 itemCenter + taskItemBgcontentStyle 固定为 display:flex; padding:8px; justify-content:space-between; align-items:center,不引用 Season5 的任务类型图标和票券图标;右侧任务按钮也改为 itemCenter 背景图 + 翻译文字Gulben 页面中任务列表和领取任务等涉及 activityId 的接口统一固定传 gulbenActivityId 的值,也就是字符串 '1001'
  • 补充 Personal Rank / Reward 预览结构:只展示 RewardTop1RewardTop2RewardTop3 三个 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 图片根据 vipLevelNameVISCOUNT->vip1EARL->vip2MARQUIS->vip3DUKEI->vip4KING->vip5EMPEROR->vip6 映射展示,没有 vipLevelName 时不展示userLevel 背景图根据 charmLevel 按 1-10/11-20/21-30/31-40/41-50 对应 userLevel1Bg~userLevel5BgwealthLevel 背景图根据 wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg,内部居中展示对应等级文字,文字白色且 font-size:1em;切换到该标签时贴底展示我的排名,并通过底部 rankingLoadmore 上拉加载更多
  • Personal Rank / RewardPersonal Rank / Ranking 的活动奖励、活动礼物都来自 /activity/king-queen/effective?templateId=2057676469390872578;页面内必须通过共享缓存方法读取,避免进入 Reward 和 Ranking 时重复请求同一个 effective 接口。
  • 补充 Room Rank / Reward 和 RankingReward 直接 100vw 展示 ruleActivityRanking 复用 Personal Rank 的排行榜展示和 weekly-star 上拉分页,但不展示活动礼物模块,头像统一 border-radius:4px列表和我的排名只展示名称、ID、金币数量不展示 vip1userLevel1BgwealthLevel1Bg 三个等级图片
  • 创建本地 OSS 目录 public/oss/h5/Azizi/Activities/GulbenFestival/
  • 添加路由 /activities/gulben-festival
  • 添加安全白名单和活动页权限片段

未填写所以跳过:

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

待用户微调:

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

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

本地图片目录:public/oss/h5/Azizi/Activities/GulbenFestival/

路由 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 -> rankuserProfile.userAvatar -> userAvataruserProfile.userNickname -> userNameuserProfile.ownSpecialId.account 优先作为展示账号,不存在时使用 userProfile.accountuserProfile.inRoomId -> roomIdtotalFormat -> amount
  • 排行榜点击逻辑仿照 src/views/Ranking/Overall/Ranking.vueRoom Rank 的前三名、第四名开始的列表项点击时使用 userProfile.inRoomId 调用 src/utils/appBridge.js 中的 gotoRoom(roomId)Personal Rank 的前三名、第四名开始的列表项点击时调用 viewUserInfo(userId);贴底的我的排名不绑定点击事件。

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

  • 已重新扫描 public/oss/h5/Azizi/Activities/GulbenFestival/ 下的图片,并同步 src/views/Activities/GulbenFestival/page.config.jspreload
  • blocking 保持首屏关键图片:bgruleBtBgtimeBgtagActiveBgtagBglotteryBgcollectGameBg
  • background 已补齐当前目录中除首屏关键图外的全部 .png 图片:barrageBgcardBackBgdraw1Btdraw5BtBgGameDrawBtBgGameticketgameTitleRgameTitleSRgameTitleSSRgiftBghelpBtleftBtlotteryPrizeActiveBglotteryPrizeBgmoduleActiveBgmoduleBgRankingItemredeemR2001~redeemR2005redeemSR2011~redeemSR2013redeemSSR2021~redeemSSR2022RewardTop1~RewardTop3ruleActivityruleInfoBgtaskBtGotaskBtReceivetaskBtReceivedtaskItemBgticketHandleBtBgticketRticketSRticketSSRtop1~top3vip1~vip6userLevel1Bg~userLevel5BgwealthLevel1Bg~wealthLevel5Bg
  • 已移除 page.config.js 预加载中不存在于本地目录的旧素材名:redeemRGiftredeemRGoatredeemRLampredeemRLightredeemRMoonredeemSRLambredeemSRScrollredeemSRTurkeyredeemSSRGoatredeemSSRLamb
  • 页面本地 fallback 奖励图也改为真实存在的卡片图片:redeemR2001~redeemR2005redeemSR2011;每周礼物占位图改为 redeemR2001redeemSR2011redeemSSR2021

本次同步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:4px列表和我的排名只展示名称、ID、金币数量不展示 vip1userLevel1BgwealthLevel1Bg

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

  • /activity/card/reward/draw 返回数据按 body.rewards 解析;兼容旧的 body.results 和直接数组返回,最终统一存入 drawResult.results
  • 单抽和 5 连抽接口成功后先播放奖励池模拟转圈动画,动画结束后再展示 resultBg 结果弹窗。
  • 结果弹窗图片优先取 rewards 中第一个 isWin === true 的奖品;如果没有中奖标记,则退回使用第一条结果;奖励池抽奖结果图宽度为 45vw,抽卡/合成结果图继续使用 28vw
  • 结果弹窗底部按钮文本为 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 展示选中态。
  • 由于 lotteryPrizeActiveBglotteryPrizeBg 只有颜色差异,使用覆盖图透明度切换可以避免动画过程中反复改图片 src,减少移动端图片重新解码、布局重算导致的页面抖动。
  • 奖品图片和奖励信息统一放到覆盖图上层,使用 position:relative; z-index:1 保持显示层级。

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

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

本次同步:抽奖动画防抖动

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

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

  • lotteryPrizeActiveBg 仍然用于抽奖选中态视觉,但不再作为 itemCenterimgUrl 频繁切换。
  • 奖品格 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 图。
  • 点击抽奖后动画从当前 activeLotteryIndex 位置继续向后滚动,最终停留在接口返回中奖奖品对应的位置;下一次抽奖继续从上一次停留位置开始模拟转圈。

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

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

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

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

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

  • 一级标签按钮不再通过 :imgUrl="tabButtonImage(tab.value)"tagBgtagActiveBg 之间切换背景图。
  • 一级标签 itemCenter 底图固定为 imageUrl('tagBg'),选中态 tagActiveBg 作为内部 tab-active-bg 覆盖图,通过 v-show="activeTopTab === tab.value" 控制显示。
  • 二级标签按钮不再通过 :imgUrl="childTabButtonImage(tab.value)"moduleBgmoduleActiveBg 之间切换背景图。
  • 二级标签 itemCenter 底图固定为 imageUrl('moduleBg'),选中态 moduleActiveBg 作为内部覆盖图,通过 v-show="activeChildTab === tab.value" 控制显示。
  • .tab-background 增加 overflow:hiddencontain: layout paintisolation:isolatetransform: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.jspreload.blocking 补充标签内容区关键大图:moduleActiveBg/moduleBgtaskItemBgRewardTop1~3ruleActivitygiftBgRankingItemtop1~3,尽量在进入页面前完成核心标签页素材加载。