77 KiB
77 KiB
页面创建信息
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/ - 本地开发目录:
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 四个 ref,calculateCountdown 立即计算并由 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/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,任务列表调用 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}¤t=${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 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": ["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 改为 4px;第四名以后和我的排名使用 RankingItem,只展示排名、头像、名称、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';任务列表调用 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,列表项使用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:4px,列表和我的排名只展示名称、ID、金币数量,不展示vip1、userLevel1Bg、wealthLevel1Bg三个等级图片 - 创建本地 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 -> 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/下的图片,并同步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:4px,列表和我的排名只展示名称、ID、金币数量,不展示vip1、userLevel1Bg、wealthLevel1Bg。
本次同步:奖励池抽奖结果弹窗
/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展示选中态。- 由于
lotteryPrizeActiveBg和lotteryPrizeBg只有颜色差异,使用覆盖图透明度切换可以避免动画过程中反复改图片src,减少移动端图片重新解码、布局重算导致的页面抖动。 - 奖品图片和奖励信息统一放到覆盖图上层,使用
position:relative; z-index:1保持显示层级。
本次同步:抽奖高亮防抖动方案
lotteryBg奖品格子的itemCenter底图固定使用lotteryPrizeBg,抽奖动画时不再通过切换:imgUrl或额外 active 图片节点来切换背景。- 当前选中奖品只切换
lottery-prize-item-activeclass;高亮效果由.lottery-prize-item::after的叠色和阴影实现,动画中只改变opacity,减少图片src变化、图片解码和布局重算导致的页面抖动。 - 因
lotteryPrizeActiveBg与lotteryPrizeBg实际只有颜色差异,当前优先使用 CSS 叠色模拟 active 态;如果后续视觉要求完全一致,再微调叠色颜色、透明度和阴影参数。
本次同步:抽奖动画防抖动
lotteryBg奖品格背景不再通过:imgUrl="lotteryPrizeImage(prize.index)"在lotteryPrizeBg和lotteryPrizeActiveBg之间频繁切换。- 奖品格
itemCenter固定使用imageUrl('lotteryPrizeBg'),选中态只切换lottery-prize-item-activeclass。 - 高亮效果通过
.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 图。 - 点击抽奖后动画从当前
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,尽量在进入页面前完成核心标签页素材加载。