# 页面创建信息 ## 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` 初版结构。 ```json { "tabs": { "enabled": true, "defaultValue": "CollectCards to Win Prizes", "items": [ "CollectCards to Win Prizes", "Personal Rank", "Room Rank" ] }, "sections": [ { "name": "collectCards", "enabled": true, "apis": [], "images": [ "tagActiveBg", "tagBg" ], "remark": "一级标签内容区;图片为背景图,使用 itemCenter 组件居中换行展示英文标题文本,开发期间不加翻译", "children": {} }, { "name": "personalRank", "enabled": true, "apis": [], "images": [ "tagActiveBg", "tagBg" ], "remark": "一级标签内容区;图片为背景图,使用 itemCenter 组件居中换行展示英文标题文本,开发期间不加翻译", "children": { "tabs": { "enabled": true, "defaultValue": "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 全局顶部和规则入口 ```json { "enabled": true, "activityTime": "", "images": ["bg", "ruleBtBg", "ruleInfoBg", "ruleActivity"], "apis": [], "fields": { "startTime": "", "endTime": "2026-06-01 05:00:00" }, "actions": { "rule": "点击规则按钮图片打开规则弹窗" }, "popup": { "component": "MaskLayer", "backgroundImage": "ruleInfoBg", "contentComponent": "itemCenter", "closeRule": "点击遮罩关闭,点击弹窗内容阻止冒泡", "contentStyle": "padding:18vw 7vw 12vw; align-items:stretch; justify-content:flex-start; box-sizing:border-box", "textStyle": "max-height:62vh; overflow-y:auto; color:#FFF; font-size:1em; font-weight:600; line-height:1.35; text-align:left", "scrollbar": "内容区可上下滑动,但使用 rule-popup-scroll 隐藏滚动条;需要 class 处理 scrollbar-width / -ms-overflow-style / ::-webkit-scrollbar", "renderRule": "规则文案不要依赖源码空白或 white-space:pre-line;按标题和 (1)/(2)/(3) 拆成独立 div 行,保证 1、2、3、4 和每条小规则都单独换行", "content": "1、How to Earn Tickets:\n(1)Complete designated series of tasks to earnTickets.\n2、How to Use Tickets:\n(1)Each Ticket can be used to randomly drawone R-grade Card.\n3、Card Combining& Upgrade:\n(1)Combine 5 different R-grade Cards to randomlydraw one SR-grade Card.\n(2)Combine 3 different SR-grade Cards torandomly draw one SSR-grade Card.\n4、Collect & Redeem Rewards:\n(1)Collect 5 different R-grade Cards to redeem onechance to draw a Standard Reward.\n(2)Collect 3 different SR-grade Cards to redeemone chance to draw a Premium Reward.\n(3)Collect 2 different SSR-grade Cards to redeemone chance to draw a Deluxe Reward!" }, "remark": "规则弹窗用 src/components/MaskLayer.vue,写法参考 src/views/Activities/LuckyDollars/Season5/index.vue。" } ``` ### 5.2 倒计时模块 ```json { "name": "countdown", "enabled": "", "deadline": "", "images": ["timeBg"], "fields": { "endTime": "2026-06-01 05:00:00", "remainingText": "" }, "remark": "倒计时方法参考 src/views/Activities/LuckyDollars/Season5/index.vue:使用 Days / Hours / Minutes / Seconds 四个 ref,calculateCountdown 立即计算并由 startCountdown 每秒刷新;天数在模板补 D,时分秒补两位;目标时间直接在 getTargetTime 中使用 new Date(2026, 5, 1, 5, 0, 0),不解析中文;时分秒的中间用英文冒号:" } ``` ### 5.3 一级标签:CollectCards to Win Prizes ```json { "sectionName": "collectCards", "tabText": "CollectCards to Win Prizes", "enabled": true, "apis": { "activityDetail": "activityDetail(activityCode):参考 LuckyDollars Season5 的 /activity/lottery/detail/${activityCode}", "rewardDraw": "apiPostCardRewardDraw({ activityId:'1001', ticketType, drawCount }):POST /activity/card/reward/draw;lotteryBg 下层 draw1Bt / draw5BtBg 使用,ticketType 为 STANDARD/PREMIUM/LUXURY,对应 R/SR/SSR 奖池,drawCount 为 integer 格式的 1 或 5;请求成功后刷新 apiGetCardTicketList('1001')", "drawOnce": "apiPostCardDraw({ activityId:'1001', drawCount, needCoins }):POST /activity/card/draw;点击 collectGameBg 的 GameDrawBtBg 时调用,drawCount 默认为 1;当 CARD 抽卡票数量为 0 时 needCoins=true,否则 needCoins=false", "drawFive": "", "combineCard": "apiPostCardCombine({ activityId:'1001', cardTemplateIds }):POST /activity/card/combine;R 奖池需要 5 个不同 R 卡 templateId 合成 1 张 SR,SR 奖池需要 3 个不同 SR 卡 templateId 合成 1 张 SSR;SSR 奖池不展示 Combine", "redeemCard": "apiPostCardRedeem({ activityId:'1001', cardTemplateIds }):POST /activity/card/redeem;将当前奖池不同卡片兑换为对应奖励票,R 需要 5 个不同 R 卡、SR 需要 3 个不同 SR 卡、SSR 需要 2 个不同 SSR 卡;成功后刷新票券数量和当前卡包", "ticketList": "apiGetCardTicketList('1001'):GET /activity/card/ticket/list?activityId=1001", "rewardPrizeList": "apiGetCardRewardPrizeList('1001', poolType):GET /activity/card/reward/prize/list?activityId=1001&poolType=${poolType};初始化时按 STANDARD / PREMIUM / LUXURY 三个 poolType 分别调用,对应页面展示 R / SR / SSR", "rewardMarquee": "apiGetCardRewardMarquee('1001'):GET /activity/card/reward/marquee?activityId=1001", "cardBagList": "apiPostCardBagList({ activityId:'1001', cardGrade, current:1, size:20 }):POST /activity/card/bag/list;初始化时按 R / SR / SSR 三个 cardGrade 分别调用,接口虽然是 POST 但用于查询用户卡包返回数据", "taskList": "", "receiveTaskReward": "", "winnerHistory": "" }, "images": { "container": ["lotteryBg", "collectGameBg", "taskModule"], "lottery": [ "lotteryPrizeBg", "lotteryPrizeActiveBg", "draw1Bt", "draw5BtBg"], "collectGame": ["cardBackBg", "GameDrawBtBg", "redeemR2001", "redeemR2002", "redeemR2003", "redeemR2004", "redeemR2005", "redeemSR2011", "redeemSR2012", "redeemSR2013", "redeemSSR2021", "redeemSSR2022"], "tickets": ["ticketR", "ticketSR", "ticketSSR", "ticketHandleBtBg", "helpBt"], "titles": ["gameTitleR", "gameTitleSR", "gameTitleSSR"], "tasks": ["taskItemBg", "taskBtGo", "taskBtReceive", "taskBtReceived", "Gameticket"], "ranking": ["RankingItem", "vip1", "userLevel1Bg", "wealthLevel1Bg"], "barrage": ["barrageBg"] }, "fields": { "ticketCount": "", "cardBagPools": "按 cardGrade 存储 /activity/card/bag/list 返回的 body.records;R/SR/SSR 分别对应集卡奖池的卡片正面展示", "cardList": "", "lotteryPrizeList": "", "taskList": "", "barrageList": "" }, "actions": { "rewardDraw": "点击 lotteryBg 下层 draw1Bt 调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 }),当前奖池票券数量为 0 时按钮置灰且点击无效;点击 draw5BtBg 调用 drawCount:5,当前奖池票券数量不足 5 时按钮置灰且点击无效;接口成功后刷新 apiGetCardTicketList('1001') 同步第一层三种票券数量,先根据接口结果播放 lotteryPrizeActiveBg 模拟转圈动画,动画结束后再展示 resultBg 结果弹窗,底部按钮文本为 Confirm,点击后关闭弹窗", "drawOnce": "点击 GameDrawBtBg 调用 apiPostCardDraw({ activityId:'1001', drawCount:1, needCoins: cardDrawTicketCount === 0 });接口成功后刷新 apiGetCardTicketList('1001') 和 R 级 apiPostCardBagList({ activityId:'1001', cardGrade:'R', current:1, size:20 })", "drawFive": "", "combineCard": "点击 Combine 调用 apiPostCardCombine({ activityId:'1001', cardTemplateIds });cardTemplateIds 为当前奖池用户已拥有且数量大于 0 的不同卡片 templateId 字符串数组;R 需要 5 个不同 R 卡,SR 需要 3 个不同 SR 卡;成功后刷新当前等级卡包和目标等级卡包", "redeemCard": "点击 Redeem 调用 apiPostCardRedeem({ activityId:'1001', cardTemplateIds });cardTemplateIds 为当前奖池用户已拥有且数量大于 0 的不同卡片 templateId 字符串数组;R/SR/SSR 分别需要 5/3/2 个不同卡片;成功后刷新 apiGetCardTicketList('1001') 和当前等级卡包", "goTask": "", "receiveTaskReward": "" }, "layout": { "containerRender": "container 大背景图使用 itemCenter 渲染", "containerStyle": "width:100vw; margin:0; padding:0", "wrapperStyle": "display:flex; flex-direction:column; align-items:center; gap:0; margin:0; padding:0", "contentStyle": "padding:0", "lotteryBg": { "component": "itemCenter", "contentStyle": "padding:8vw 5vw 0; align-items:stretch; justify-content:flex-start; box-sizing:border-box", "innerStyle": "width:100%; height:100%; display:flex; flex-direction:column; gap:0", "topLayer": { "style": "display:flex; flex:1; align-items:center; gap:4vw", "sideWidth": "10vw", "leftButton": "leftBt", "rightButton": "leftBt 水平翻转", "buttonImageStyle": "width:8vw; display:block" }, "prizes": { "modes": { "lotteryR": { "count": 7, "rowCounts": [2, 3, 2], "ticketImage": "ticketR", "remark": "R 奖池当前使用 7 个测试奖品,按上层 2 个、中间 3 个、下层 2 个渲染;后续由接口返回数组替换测试数据" }, "lotterySR": { "count": 3, "rowCounts": [3], "ticketImage": "ticketSR", "listClass": "lottery-prize-list-large", "itemClass": "lottery-prize-item-sr", "remark": "SR 奖池当前使用 3 个测试奖品,一行居中展示 3 个;通过缩小奖品间距和单独奖品 class 放大奖品背景图;后续由接口返回数组替换测试数据" }, "lotterySSR": { "count": 2, "rowCounts": [2], "ticketImage": "ticketSSR", "listClass": "lottery-prize-list-large", "itemClass": "lottery-prize-item-ssr", "remark": "SSR 奖池当前使用 2 个测试奖品,一行居中展示 2 个;使用比 SR 更大的奖品宽度;后续由接口返回数组替换测试数据" } }, "layout": "flex-column rows + row flex center", "gap": "R 使用 3vw;SR/SSR 使用 lottery-prize-list-large 将间距缩小为 1vw", "itemWidth": "R/SR 使用 calc((100% - var(--lottery-prize-gap) * 2) / 3);SSR 使用 calc((100% - var(--lottery-prize-gap)) / 2.2)", "modeSwitch": "左右 leftBt 切换 lotteryR / lotterySR / lotterySSR,切换时重置 activeLotteryIndex;奖励池抽奖请求中暂不允许切换奖池", "defaultImage": "lotteryPrizeBg", "activeImage": "lotteryPrizeActiveBg", "apiData": "有 /activity/card/reward/prize/list 接口数据时优先使用接口返回数组;字段为 id、poolType、prizeName、prizeType、prizeValue、prizeImage、sortOrder;按 sortOrder 升序排序,并补充当前渲染 index 用于 lotteryPrizeActiveBg 高亮动画", "itemContent": "每个奖品仍使用 itemCenter,背景根据 activeLotteryIndex 切换 lotteryPrizeBg / lotteryPrizeActiveBg;itemCenter 内部使用垂直 flex,第一行 prizeImage 宽度 60%,第二行展示奖励信息;奖励信息有 days 时展示 `${days} days`,没有 days 时展示 prizeValue,不展示 prizeName", "remark": "奖品区域按行渲染,每一行使用 flex 居中,避免 grid 在最后一行数量不足时贴左;没有接口数据时保留本地测试奖品占位,方便预览。" }, "animation": { "reference": "/activities/lucky-dollars-season5", "mode": "当前奖励池抽奖接口成功后先播放 lotteryPrizeActiveBg 模拟转圈动画,动画结束后再展示 resultBg 结果弹窗", "activeRule": "同一时间只有一个奖品 itemCenter 使用 lotteryPrizeActiveBg,其余使用 lotteryPrizeBg", "order": "从左到右、从上到下依次变化背景图", "resultFlow": "点击抽奖后先请求接口,接口返回中奖奖品,再根据奖品 id/name 计算最终位置,然后启动动画并停在中奖位置", "multiDraw": "5 连抽也直接展示结果弹窗,弹窗图片取接口返回结果中的第一个中奖奖品;没有中奖标记时取第一条结果", "currentPreview": "requestLotteryDraw 已接入 /activity/card/reward/draw;接口成功后先计算中奖停点并播放模拟动画,动画结束后再展示 resultBg 结果弹窗,底部按钮文本为 Confirm,点击后关闭弹窗", "performanceNote": "当前奖品数量少,直接切换两个 itemCenter 的 imgUrl 可以满足效果;后续如果奖品数量增加、动画频率提高或低端机出现闪烁/卡顿,优先考虑改成固定默认背景 + 单个 active 覆盖层,或用 class 切换预加载后的 CSS background,以减少图片 src 变更。", "decisionRule": "只要满足效果和布局,优先选择更省性能、用户体验更稳定的方案;如果 Codex 发现更好的样式或动画实现,需要先说明方案、优缺点和影响范围,并询问是否采用,不能静默替换用户已调好的结构。" }, "bottomLayer": { "style": "display:flex; align-items:center; justify-content:space-around; gap:0; margin:0; padding:0", "ticketIcon": "两个抽奖按钮内部都放当前奖池 ticketImage,图片宽度 10vw;itemCenter contentStyle 使用 padding:0; align-items:flex-end; 保持水平居中并垂直贴底;点击左右切换 lotteryR / lotterySR / lotterySSR 时同步切换 ticketR / ticketSR / ticketSSR", "buttons": [ { "image": "draw1Bt", "style": "width:38vw; margin:0; padding:0", "contentStyle": "padding:0; align-items:flex-end", "enabledRule": "当前奖池 poolType 对应的 STANDARD/PREMIUM/LUXURY 票券数量 >= 1 才可点击,否则添加 collect-action-disabled 置灰且点击无效", "action": "点击调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 });成功后刷新 ticketList" }, { "image": "draw5BtBg", "style": "width:38vw; margin:0; padding:0", "contentStyle": "padding:0; align-items:flex-end", "enabledRule": "当前奖池 poolType 对应的 STANDARD/PREMIUM/LUXURY 票券数量 >= 5 才可点击,否则添加 collect-action-disabled 置灰且点击无效", "action": "点击调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:5 });成功后刷新 ticketList" } ] } }, "collectGameBg": { "component": "itemCenter", "contentStyle": "padding:0; align-items:stretch; justify-content:flex-start; box-sizing:border-box", "innerStyle": "width:100%; height:100%; display:flex; flex-direction:column; gap:0; margin:0; padding:0", "ticketLayer": { "style": "height:10vw; display:flex; align-items:center; justify-content:center; gap:2vw; padding:0; margin:0", "items": [ { "image": "ticketR", "ticketType": "STANDARD" }, { "image": "ticketSR", "ticketType": "PREMIUM" }, { "image": "ticketSSR", "ticketType": "LUXURY" } ], "itemStyle": "flex:1; min-width:0; height:100%; display:flex; align-items:center; justify-content:center; gap:1vw", "help": "第一个单位内贴顶放 2vw 宽度 helpBt", "ticketImageStyle": "width:10vw; display:block", "text": ":remainingCount,默认 0", "textStyle": "color:#fff; font-size:1em; font-weight:700" }, "tipLayer": { "text": "*Redeem Your Cards and Draw for Amazing Rewards!", "style": "color:#D2D2D2; font-size:1em; font-weight:600; text-align:center; padding:0; margin:0" }, "titleLayer": { "height": "10vw", "titles": { "gameR": "gameTitleR", "gameSR": "gameTitleSR", "gameSSR": "gameTitleSSR" }, "imageStyle": "height:10vw; width:auto; display:block" }, "prizeLayer": { "modes": { "gameR": 5, "gameSR": 3, "gameSSR": 2 }, "sideWidth": "10vw", "leftButton": "leftBt", "rightButton": "leftBt 水平翻转", "centerStyle": "flex:1; min-width:0; height:20vw; display:flex; align-items:center; justify-content:space-around; gap:0", "cardImage": "cardBackBg", "cardStyle": "height:100%; width:auto; display:block", "cardBagApi": { "endpoint": "POST /activity/card/bag/list", "params": { "activityId": "'1001'", "cardGrade": "R / SR / SSR", "current": 1, "size": 20 }, "responseRecords": "body.records[];字段包含 cardTemplateId、cardGrade、cardName、cardImage、quantity", "imageMap": { "2001": "redeemR2001", "2002": "redeemR2002", "2003": "redeemR2003", "2004": "redeemR2004", "2005": "redeemR2005", "2011": "redeemSR2011", "2012": "redeemSR2012", "2013": "redeemSR2013", "2021": "redeemSSR2021", "2022": "redeemSSR2022" }, "renderRule": "卡位固定渲染,R 固定 5 张、SR 固定 3 张、SSR 固定 2 张;接口 records 中存在对应 cardTemplateId 时,将该卡位从 cardBackBg 替换为映射的本地 redeem 图片;未返回或未映射的卡位继续展示 cardBackBg,避免接口数据数量变化导致布局跳动。", "quantityBadge": "除 cardBackBg 之外的卡片正面图片都使用 itemCenter 组件承载,组件 style 使用 height:100%; width:auto; aspect-ratio:159/261; flex:0 0 auto;内部绝对定位右上角展示 records.quantity,角标样式为 border-radius:33px 33px 33px 34px; background:rgba(0,0,0,0.10); backdrop-filter:blur(2px); padding:4px; color:#fff;cardBackBg 仍然使用普通 img,不展示数量角标。" }, "remark": "点击左右按钮切换 gameR / gameSR / gameSSR;第三层标题、第四层卡片正反面和第五层操作层跟随当前模式。" }, "actionLayer": { "style": "display:flex; align-items:center; justify-content:space-around; gap:0; padding:0; margin:0", "buttons": [ { "image": "GameDrawBtBg", "style": "width:40vw; margin:0; padding:0", "contentStyle": "padding:0; flex-direction:column; gap:0.5vw; align-items:center; justify-content:center", "content": ["Remaining Tickets: *CARD.remainingCount", "1000/coins"], "action": "点击调用 /activity/card/draw,data 为 { activityId:'1001', drawCount, needCoins },drawCount 默认为 1;当 CARD.remainingCount 为 0 时 needCoins=true,否则 needCoins=false;请求成功后调用 ticketList 刷新 CARD.remainingCount,并只刷新 R 级 cardBagList 更新当前 R 卡 quantity" }, { "image": "ticketHandleBtBg", "style": "width:25vw; margin:0; padding:0", "contentStyle": "padding:0; align-items:center; justify-content:center", "content": ["Combine * 1"], "visibleRule": "SSR 奖池不展示 Combine", "enabledRule": "R 奖池用户需要拥有 5 个不同 R 卡,SR 奖池用户需要拥有 3 个不同 SR 卡;不同卡片按当前奖池固定 cardTemplateIds 中 quantity > 0 的卡片计算,不满足时添加 collect-action-disabled 样式置灰且点击无效", "action": "点击调用 /activity/card/combine,data 为 { activityId:'1001', cardTemplateIds },cardTemplateIds 为字符串数组;R 合成成功后刷新 R 和 SR 卡包,SR 合成成功后刷新 SR 和 SSR 卡包" }, { "image": "ticketHandleBtBg", "style": "width:25vw; margin:0; padding:0", "contentStyle": "padding:0; flex-direction:column; gap:0.5vw; align-items:center; justify-content:center", "content": ["Redeem", "当前奖池票券图(ticketR/ticketSR/ticketSSR) + *1"], "enabledRule": "R/SR/SSR 奖池分别需要拥有 5/3/2 个不同卡片;不同卡片按当前奖池固定 cardTemplateIds 中 quantity > 0 的卡片计算,不满足时添加 collect-action-disabled 样式置灰且点击无效", "action": "点击调用 /activity/card/redeem,data 为 { activityId:'1001', cardTemplateIds },cardTemplateIds 为字符串数组;成功后立即刷新 ticketList 和当前等级 cardBagList,使第一层三种票券数量和当前卡片 quantity 同步更新" } ] } , "taskModule": { "component": "itemCenter", "itemImage": "taskItemBg", "source": "任务接口和字段先参考 src/views/Activities/LuckyDollars/Season5/index.vue 的 taskList、ActTaskList、receiveTickets、showTarget", "data": "当前 Gulben 页面固定 activityId=1001,任务列表调用 ActTaskList('1001'),领取调用 receiveTickets({ activityId:'1001', taskCode })", "wrapperStyle": "width:100vw; display:flex; flex-direction:column; gap:2vw; margin:0; padding:0 5vw; box-sizing:border-box", "itemStyle": "width:100%; margin:0; padding:0", "contentStyle": "display:flex; padding:8px; justify-content:space-between; align-items:center", "layout": "v-for=\"task in taskList\";左侧展示 showTarget(task) 和 Rewards:*rewardValue;右侧使用 itemCenter 展示 taskBtGo / taskBtReceive / taskBtReceived,并显示 t('task_go') / t('task_receive') / t('task_received');不展示 Season5 中的任务类型图标和票券图标", "actions": "taskStatus === 1 时点击 taskBtReceive 的 itemCenter 调用 receiveTaskReward(task.taskCode)" } } }, "remark": "补集卡/抽奖/任务的数据来源和按钮行为。CollectCards to Win Prizes 当前从上到下拆成 lotteryBg、collectGameBg 两个 container 大背景模块;初始不加边框虚线,padding、margin、gap 先设为 0,后续再按预览微调。" } ``` ### 5.4 一级标签:Personal Rank ```json { "sectionName": "personalRank", "tabText": "Personal Rank", "enabled": true, "children": { "Reward": { "enabled": true, "apis": { "rewardConfig": "apiGetActivityResource:/props-activity-cnf/client/listActivityResource?sysOrigin=LIKEI&activityType=${rewardActivityType}", "receiveReward": "" }, "images": { "tab": ["moduleBg", "moduleActiveBg"], "rewardBg": ["RewardTop1", "RewardTop2", "RewardTop3"], "rewardItems": ["redeemRGift", "redeemRGoat", "redeemRLamp", "redeemRLight", "redeemRMoon", "redeemSRLamb", "redeemSRScroll", "redeemSRTurkey", "redeemSSRGoat", "redeemSSRLamb"] }, "fields": { "rewardList": "apiGetActivityResource 返回数组的前 3 套;每套 propsGroup.activityRewardProps 最多取 6 个", "receiveStatus": "", "threshold": "" }, "actions": { "receiveReward": "" }, "layout": { "container": "Personal Rank / Reward 标签只展示 RewardTop1、RewardTop2、RewardTop3 三个 itemCenter,宽度均为 100vw,从上往下排序", "source": "布局模仿 src/views/Activities/DailyRecharge/index.vue 的奖励展示模块:左侧一个主奖励,右侧网格奖励", "dataRule": "接口返回 10 套奖励时只取前 3 套;每套最多 6 个奖励,第 1 个作为左侧大图,后 5 个放右侧;如果某套只有 5 个奖励,则复制第 5 个奖励补到第 6 个位置用于预览铺满右侧第三行", "rightGrid": "右侧奖励固定分 3 行,前两行每行 2 个,第三行使用单个展示位宽度并居中;即使第三行没有奖励,也保留一个不可见占位,避免模块高度和居中位置跳动", "display": "奖励图使用 reward.cover,图片错误处理使用 handleRewardImageError;奖励文案统一使用 formatRewardDisplay(reward.type, reward),规则与 DailyRecharge 保持一致,不额外优先展示 reward.content", "fallback": "接口未返回时临时使用 redeemRGift / redeemRGoat / redeemRLamp / redeemRLight / redeemRMoon / redeemSRLamb 组成 3 套本地预览奖励" }, "remark": "补个人榜奖励配置、领取条件和领取接口;当前先展示奖励配置,不接领取。" }, "Ranking": { "enabled": true, "apis": { "rankList": "getWeekStarRanking:/activity/week-star/ranking?type=${type}¤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 ```json { "sectionName": "roomRank", "tabText": "Room Rank", "enabled": true, "children": { "Reward": { "enabled": true, "apis": { "rewardConfig": "", "receiveReward": "" }, "images": { "tab": ["moduleBg", "moduleActiveBg"], "rewardBg": ["ruleActivity"], "rewardItems": [] }, "fields": { "rewardList": "", "receiveStatus": "", "threshold": "" }, "actions": { "receiveReward": "" }, "layout": { "component": "img", "image": "ruleActivity", "style": "width:100vw; display:block", "remark": "Room Rank / Reward 暂不接奖励配置,直接 100vw 展示 ruleActivity" }, "remark": "当前按用户要求直接展示 ruleActivity;后续如果要接房间榜奖励配置再补接口。" }, "Ranking": { "enabled": true, "apis": { "rankList": "", "myRank": "" }, "images": { "tab": ["moduleBg", "moduleActiveBg"], "top": ["top1", "top2", "top3"], "list": ["RankingItem"] }, "rankConfig": { "loadMode": "remote", "pageSize": 10, "topCount": 3, "fieldMap": { "avatar": "userAvatar", "name": "userName / userNickname", "value": "amount / quantity", "roomId": "userProfile.inRoomId", "rank": "rank / ranking" } }, "layout": { "source": "展示结构复用 Personal Rank / Ranking 的排行榜部分", "difference": "不展示 Personal Rank 的活动礼物 giftBg 模块;头像统一 border-radius:4px;第四名以后和我的排名只展示排名、头像、名称、ID、金币数量,不展示 vip1 / userLevel1Bg / wealthLevel1Bg 三个等级图片", "myRanking": "切换到 Room Rank / Ranking 时同样固定贴底展示我的排名,样式跟列表一致,只保留名称、ID 和金币数量", "pagination": "使用 GET /room/contribution/total-rank?startDate=20260518&endDate=20260524&size=20;当前接口没有 current/page 参数,加载完成后 rankingHasMore=false,不再触发上拉继续请求" }, "remark": "Room Rank / Ranking 当前复用 Personal Rank 排行榜接口和布局,按房间榜展示要求裁掉礼物模块与等级图。" } } } ``` ### 5.6 接口参数 ```json { "activityId": "1001", "activityCode": "", "activityType": "", "personalRankActivityType": "THIS_WEEK", "roomRankActivityType": "", "rewardActivityType": "DAILY_RECHARGE_TASK2", "otherParams": {} } ``` ### 5.7 组件需求 ```json [ { "name": "BackgroundLayer", "enabled": "", "remark": "页面主背景 bg" }, { "name": "itemCenter", "enabled": "是", "remark": "一级/二级标签图片背景 + 居中文本;倒计时天/时/分/秒图片背景" }, { "name": "ActivityTabs", "enabled": "", "remark": "一级标签和二级标签;如不用通用组件,则按图片背景自定义" }, { "name": "ActivityLotteryGrid", "enabled": "", "remark": "CollectCards 抽奖区域" }, { "name": "Barrage", "enabled": "", "remark": "CollectCards 弹幕区域" }, { "name": "topUser / TopUser", "enabled": "", "remark": "个人榜/房间榜前三名展示;GulbenFestival 使用页面私有组件 src/views/Activities/GulbenFestival/components/topUser.vue,不直接修改或复用参考页面组件" } ] ``` ### 5.8 当前 Vue 布局与样式同步记录 说明:本节按实时 `src/views/Activities/GulbenFestival/index.vue` 反向同步,用于记录当前可预览底座已经调好的结构和样式。后续如果本轮执行方案是“md -> vue”,则以本节为源同步到 Vue;如果本轮执行方案是“vue -> md”,则以实时 Vue 为准更新本节。 ```json { "syncSource": "src/views/Activities/GulbenFestival/index.vue", "lastSyncDirection": "vue -> md", "layout": { "root": { "class": "fullPage", "styleBlock": "style lang=\"scss\" scoped 顶部固定放全局文字色、fullPage、响应式基础字号 media query" }, "loading": { "style": "display:flex; min-height:100vh; align-items:center; justify-content:center" }, "pageShell": { "class": "scroll", "dir": "isRtlLayout ? 'rtl' : 'ltr'", "style": "min-height:100vh; position:relative; overflow-x:hidden" }, "background": { "component": "BackgroundLayer", "images": ["bg"], "remark": "背景图内包含标题和顶部视觉信息" }, "content": { "tag": "main", "style": "position:relative; z-index:1; width:min(100vw, 480px); min-height:100vh; margin:0 auto; padding:100vw 4vw 12vw", "remark": "顶部 padding 预留背景标题和活动视觉信息位置" } }, "rows": [ { "name": "rule", "order": 1, "wrapperClass": "rule-row", "rtlRule": "[dir='rtl'] .rule-row { justify-content: flex-start; }", "component": "itemCenter", "image": "ruleBtBg", "style": "width:15vw; flex:0 0 15vw", "contentStyle": "padding: 1.5vw 2vw;", "text": "rule", "textStyle": "color:#fff8d7; font-size:1em; font-weight:800; line-height:1; text-align:center; text-shadow:0 1px 2px rgba(63,20,57,.5); white-space:nowrap" }, { "name": "countdown", "order": 2, "wrapperStyle": "display:flex; justify-content:center; margin-bottom:4vw", "innerStyle": "width:min(95vw, 440px); display:flex; align-items:center; justify-content:space-between; gap:1vw; padding:0 2vw; overflow:hidden", "itemClass": "countdown-cell", "itemStyle": "width:min(18vw, 88px); min-width:0; flex:0 1 min(18vw, 88px)", "valueClass": "countdown-value", "colonClass": "countdown-colon", "image": "timeBg", "contentStyle": "padding: 1.8vw 1vw;", "data": "参考 LuckyDollars Season5:Days / Hours / Minutes / Seconds 四个 ref;getTargetTime 直接返回 new Date(2026, 5, 1, 5, 0, 0).getTime(),月份从 0 开始,5 代表 6 月;calculateCountdown 负责计算并在过期后清零和停止定时器;startCountdown 立即执行一次并 setInterval 每秒刷新", "remark": "天数在模板补 D;时分秒之间使用英文冒号" }, { "name": "topTabs", "order": 3, "wrapperStyle": "display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); align-items:stretch; gap:2vw; margin:0 0 4vw", "buttonClass": "image-tab top-tab", "activeClass": "image-tab-active", "activeTextSelector": ".image-tab-active span", "activeTextColor": "#5B0000", "component": "itemCenter", "contentStyle": "padding: 0 1vw;", "images": ["tagBg", "tagActiveBg"] }, { "name": "content", "order": 4, "panelStyle": "min-height:86vw; padding:0; margin:0", "titleStyle": "", "childTabsStyle": "display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:2vw; margin-bottom:4vw", "containerImages": ["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` 规则按钮,默认贴右、阿语贴左;固定单点样式优先内联 - 补充 `