feat(古尔邦节): 调整页面

This commit is contained in:
hzj 2026-05-23 20:30:34 +08:00
parent 7186265a8b
commit c93f91801f
9 changed files with 187 additions and 58 deletions

View File

@ -467,6 +467,7 @@
"Reward": "المكافآت",
"Ranking": "الترتيب",
"gulben_collect_tip": "*استبدل بطاقاتك واسحب للحصول على مكافآت رائعة!",
"gulben_barrage_get_a": "حصل على",
"gulben_remaining_tickets": "التذاكر المتبقية: *{count}",
"gulben_coins_cost": "{count}/عملات",
"gulben_combine_count": "دمج * {count}",

View File

@ -467,6 +467,7 @@
"Reward": "পুরস্কার",
"Ranking": "র‍্যাঙ্কিং",
"gulben_collect_tip": "*আপনার কার্ড রিডিম করুন এবং দারুণ পুরস্কারের জন্য ড্র করুন!",
"gulben_barrage_get_a": "পেয়েছেন",
"gulben_remaining_tickets": "অবশিষ্ট টিকিট: *{count}",
"gulben_coins_cost": "{count}/কয়েন",
"gulben_combine_count": "কম্বাইন * {count}",

View File

@ -467,6 +467,7 @@
"Reward": "Reward",
"Ranking": "Ranking",
"gulben_collect_tip": "*Redeem Your Cards and Draw for Amazing Rewards!",
"gulben_barrage_get_a": "Get a",
"gulben_remaining_tickets": "Remaining Tickets: *{count}",
"gulben_coins_cost": "{count}/coins",
"gulben_combine_count": "Combine * {count}",

View File

@ -467,6 +467,7 @@
"Reward": "Ödül",
"Ranking": "Sıralama",
"gulben_collect_tip": "*Kartlarını kullan ve harika ödüller için çekiliş yap!",
"gulben_barrage_get_a": "Kazandı",
"gulben_remaining_tickets": "Kalan Biletler: *{count}",
"gulben_coins_cost": "{count}/coin",
"gulben_combine_count": "Birleştir * {count}",

View File

@ -467,6 +467,7 @@
"Reward": "奖励",
"Ranking": "排行",
"gulben_collect_tip": "*兑换卡片并抽取丰厚奖励!",
"gulben_barrage_get_a": "获得",
"gulben_remaining_tickets": "剩余票券:*{count}",
"gulben_coins_cost": "{count}/金币",
"gulben_combine_count": "合成 * {count}",

View File

@ -189,8 +189,89 @@
"
>
<template v-for="imageName in activeContainerImages" :key="imageName">
<!-- 弹幕模块中奖飘窗接口返回后横向展示一行 -->
<div
v-if="imageName === 'barrage' && barrageRenderList.length"
style="
width: 100vw;
height: 8vw;
overflow: hidden;
margin: 0 0 4vw 0;
padding: 0;
direction: ltr;
"
>
<div
class="barrage-track"
style="display: flex; align-items: center; gap: 3vw; height: 100%"
>
<itemCenter
v-for="(item, index) in barrageRenderList"
:key="`${item.account || item.nickName || index}-${item.drawTime || index}`"
:imgUrl="imageUrl('barrageBg')"
style="width: 45vw; height: 8vw; flex: 0 0 45vw; margin: 0; padding: 0"
contentStyle="padding: 0 2vw; display: flex; align-items: center; justify-content: flex-start; gap: 1vw; box-sizing: border-box;"
>
<img
v-smart-img.noFade
:src="item.avatar"
alt=""
style="
width: 6vw;
height: 6vw;
flex: 0 0 6vw;
border-radius: 50%;
object-fit: cover;
display: block;
"
@error="handleAvatarImageError"
/>
<div
style="
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5vw;
"
>
<div
style="
width: 100%;
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ item.nickName || item.account || '' }}
</div>
<div
style="
width: 100%;
color: #fff;
font-size: 1em;
font-weight: 600;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
"
>
{{ t('gulben_barrage_get_a') }} {{ item.prizeName || '' }}
</div>
</div>
</itemCenter>
</div>
</div>
<!-- lotteryBg -->
<itemCenter
v-if="imageName === 'lotteryBg'"
v-else-if="imageName === 'lotteryBg'"
:imgUrl="imageUrl(imageName)"
style="width: 100vw; margin: 0; padding: 0"
contentStyle="padding: 8vw 5vw 0; align-items: stretch; justify-content: flex-start; box-sizing: border-box;"
@ -385,6 +466,7 @@
</div>
</itemCenter>
<!-- collectGameBg -->
<itemCenter
v-else-if="imageName === 'collectGameBg'"
:imgUrl="imageUrl(imageName)"
@ -693,6 +775,7 @@
</div>
</itemCenter>
<!-- taskModule -->
<div
v-else-if="imageName === 'taskModule'"
style="
@ -1065,7 +1148,6 @@
:name="rankingTop3[1].userName"
:distributionValue="rankingTop3[1].amount"
:valueIconUrl="coinIcon"
:avatar-square="isRoomRankingSection"
style="width: 100%"
@click="handleRankingClick(rankingTop3[1])"
/>
@ -1078,7 +1160,6 @@
:name="rankingTop3[0].userName"
:distributionValue="rankingTop3[0].amount"
:valueIconUrl="coinIcon"
:avatar-square="isRoomRankingSection"
style="width: 100%"
@click="handleRankingClick(rankingTop3[0])"
/>
@ -1091,7 +1172,6 @@
:name="rankingTop3[2].userName"
:distributionValue="rankingTop3[2].amount"
:valueIconUrl="coinIcon"
:avatar-square="isRoomRankingSection"
style="width: 100%"
@click="handleRankingClick(rankingTop3[2])"
/>
@ -1586,17 +1666,37 @@
</div>
<div
v-if="showRewardResultSwitcher"
v-if="cardResultActionMode === 'rewardDraw'"
style="
position: relative;
width: 100%;
min-height: 1.2em;
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1;
line-height: 1.2;
text-align: center;
"
>
{{ cardDrawResultIndex + 1 }}/{{ cardDrawResultList.length }}
<span style="color: #fff; font-size: 1em; font-weight: 700">
{{ currentCardDrawResultText }}
</span>
<span
v-if="showRewardResultSwitcher"
style="
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
color: #fff;
font-size: 1em;
font-weight: 700;
line-height: 1;
white-space: nowrap;
"
>
{{ cardDrawResultIndex + 1 }}/{{ cardDrawResultList.length }}
</span>
</div>
<itemCenter
@ -1749,7 +1849,7 @@ import { preloadImages, preloadImagesIdle } from '@/utils/image/imagePreloader.j
import { showError } from '@/utils/toast.js'
import { viewUserInfo } from '@/utils/appBridge.js'
import { connectToApp } from '@/utils/appConnector.js'
import { handleRewardImageError } from '@/utils/image/imageHandler.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
import { formatRewardDisplay } from '@/utils/rewardFormatter.js'
import { formatUTCCustom } from '@/utils/utcFormat.js'
import { useDebounce } from '@/utils/useDebounce.js'
@ -1846,38 +1946,7 @@ const cardTicketList = ref([])
const cardRewardPrizePools = ref({})
const cardRewardMarqueeList = ref([])
const cardBagPools = ref({})
const taskList = ref([
{
taskCode: 'GF_MIC_TIME_30',
taskType: 1,
taskName: 'Stay on mic',
targetType: 'MIC_TIME',
currentValue: 12,
targetValue: 30,
rewardValue: 1,
taskStatus: 0,
},
{
taskCode: 'GF_SEND_GIFT_5',
taskType: 2,
taskName: 'Send 5 gifts',
targetType: 'SEND_GIFT',
currentValue: 3,
targetValue: 5,
rewardValue: 2,
taskStatus: 1,
},
{
taskCode: 'GF_RECHARGE_10',
taskType: 5,
taskName: 'Daily total recharge amount: $10',
targetType: 'RECHARGE_AMOUNT',
currentValue: 10,
targetValue: 10,
rewardValue: 3,
taskStatus: 2,
},
])
const taskList = ref([])
let countdownTimer = null
let lotteryAnimationTimer = null
@ -2121,6 +2190,17 @@ const activeContainerImages = computed(() => {
return activeSection.value?.containerImages || activeSection.value?.images?.container || []
})
const barrageRenderList = computed(() => {
const marqueeList = Array.isArray(cardRewardMarqueeList.value) ? cardRewardMarqueeList.value : []
if (!marqueeList.length) {
return []
}
return marqueeList.length === 1
? [...marqueeList, ...marqueeList, ...marqueeList, ...marqueeList]
: [...marqueeList, ...marqueeList]
})
const activeLotteryPoolMode = computed(() => {
return lotteryPoolModes[activeLotteryPoolIndex.value] || lotteryPoolModes[0]
})
@ -2287,8 +2367,10 @@ const changeLotteryPoolMode = (step) => {
activeLotteryIndex.value = 0
}
// 沿 LuckyDollars Season5
// CARD_
const showTarget = (task) => {
const taskCode = task.taskCode || ''
if (task.targetType === 'MIC_TIME') {
const timeType = task.targetValue < 60
let currentValue = timeType ? task.currentValue : task.currentValue / 60
@ -2305,7 +2387,7 @@ const showTarget = (task) => {
}
if (task.targetType === 'SEND_GIFT') {
if (task.taskCode === 'SP_SEND_GIFT_5') {
if (taskCode.endsWith('SP_SEND_GIFT_5')) {
return t('Send 5 Ramadan gifts ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
@ -2314,21 +2396,21 @@ const showTarget = (task) => {
}
if (task.targetType === 'SAVE_COINS') {
if (task.taskCode === 'SP_SAVE_50000') {
if (taskCode.endsWith('SP_SAVE_50000')) {
return t('Win 50,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (task.taskCode === 'SP_SAVE_100000') {
if (taskCode.endsWith('SP_SAVE_100000')) {
return t('Win 100,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (task.taskCode === 'SP_SAVE_500000') {
if (taskCode.endsWith('SP_SAVE_500000')) {
return t('Win 500,000 coins in total across any games ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
@ -2337,28 +2419,28 @@ const showTarget = (task) => {
}
if (task.targetType === 'RECHARGE_AMOUNT') {
if (task.taskCode === 'SP_RECHARGE_1') {
if (taskCode.endsWith('SP_RECHARGE_1')) {
return t('Daily total recharge amount: $1 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (task.taskCode === 'SP_RECHARGE_10') {
if (taskCode.endsWith('SP_RECHARGE_10')) {
return t('Daily total recharge amount: $10 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (task.taskCode === 'SP_RECHARGE_50') {
if (taskCode.endsWith('SP_RECHARGE_50')) {
return t('Daily total recharge amount: $50 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
})
}
if (task.taskCode === 'SP_RECHARGE_100') {
if (taskCode.endsWith('SP_RECHARGE_100')) {
return t('Daily total recharge amount: $100 ({currentValue}/{targetValue})', {
currentValue: task.currentValue,
targetValue: task.targetValue,
@ -2552,6 +2634,31 @@ const currentCardDrawResultImageUrl = computed(() => {
)
})
const currentCardDrawResult = computed(() => {
return cardDrawResultList.value[cardDrawResultIndex.value] || null
})
const currentCardDrawResultText = computed(() => {
if (cardResultActionMode.value !== 'rewardDraw') {
return ''
}
const result = currentCardDrawResult.value
if (!result) {
return ''
}
const days = result?.prize?.days ?? result.days
if (days !== undefined && days !== null && days !== '') {
return t('reward_days', { quantity: days })
}
const prizeValue = result?.prize?.prizeValue ?? result.prizeValue
return prizeValue !== undefined && prizeValue !== null && prizeValue !== ''
? String(prizeValue)
: ''
})
const showRewardResultSwitcher = computed(() => {
return cardResultActionMode.value === 'rewardDraw' && cardDrawResultList.value.length > 1
})
@ -3647,6 +3754,22 @@ onUnmounted(() => {
}
/* 第四行:当前标签内容区 */
.barrage-track {
width: max-content;
animation: gulben-barrage-scroll 80s linear infinite;
will-change: transform;
}
@keyframes gulben-barrage-scroll {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-100%);
}
}
.pool-arrow {
width: 8vw;
display: block;

View File

@ -115,7 +115,7 @@ export const pageConfig = {
enabled: true,
apis: [],
images: ['tagActiveBg', 'tagBg'],
containerImages: ['lotteryBg', 'collectGameBg', 'taskModule'],
containerImages: ['barrage', 'lotteryBg', 'collectGameBg', 'taskModule'],
previewImages: [],
remark:
'一级标签内容区container 大背景图使用 itemCenter 组件,宽度 100vw从上往下排列开发期 padding、margin、gap 先设为 0后续再微调。',

View File

@ -568,7 +568,7 @@
"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 })",
"data": "当前 Gulben 页面固定 activityId=1001taskList 默认空数组;任务列表调用 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",
@ -708,7 +708,7 @@
},
"layout": {
"source": "展示结构复用 Personal Rank / Ranking 的排行榜部分",
"difference": "不展示 Personal Rank 的活动礼物 giftBg 模块;头像统一 border-radius:4px第四名以后和我的排名只展示排名、头像、名称、ID、金币数量不展示 vip1 / userLevel1Bg / wealthLevel1Bg 三个等级图片",
"difference": "不展示 Personal Rank 的活动礼物 giftBg 模块;前三名头像保持 border-radius:50%,第四名以后和我的排名头像使用 border-radius:4px第四名以后和我的排名只展示排名、头像、名称、ID、金币数量不展示 vip1 / userLevel1Bg / wealthLevel1Bg 三个等级图片",
"myRanking": "切换到 Room Rank / Ranking 时同样固定贴底展示我的排名样式跟列表一致只保留名称、ID 和金币数量",
"pagination": "使用 GET /room/contribution/total-rank?startDate=20260518&endDate=20260524&size=20当前接口没有 current/page 参数,加载完成后 rankingHasMore=false不再触发上拉继续请求"
},
@ -846,7 +846,7 @@
"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"],
"containerImages": ["barrage", "lotteryBg", "collectGameBg", "taskModule"],
"containerWrapperStyle": "display:flex; flex-direction:column; align-items:center; gap:0; margin:0; padding:0",
"containerItemComponent": "itemCenter",
"containerItemStyle": "width:100vw; margin:0; padding:0",
@ -906,7 +906,7 @@
"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 等级图",
"difference": "不展示活动礼物 giftBg 模块;前三名仍用 GulbenFestival 私有 TopUser 组件和 top1/top2/top3头像保持 border-radius:50%;第四名以后和我的排名使用 RankingItem,头像使用 border-radius:4px只展示排名、头像、名称、ID、金币数量不展示 vip1 / userLevel1Bg / wealthLevel1Bg 等级图",
"data": "Room Rank / Ranking 固定请求 GET /room/contribution/total-rank参数 startDate=20260518、endDate=20260524、size=20rankList 渲染排行榜current 渲染我的房间榜;展示账号优先取 userProfile.ownSpecialId.account没有时取 userProfile.account贡献值取 totalFormat点击房间榜条目时使用 userProfile.inRoomId 调用 src/utils/appBridge.js 的 gotoRoom 方法"
},
"taskModuleLayout": {
@ -917,7 +917,7 @@
"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 })"
"data": "当前 Gulben 页面所有涉及 activityId 的接口调用都固定传 gulbenActivityId 的值,也就是字符串 '1001'taskList 默认空数组;任务列表调用 ActTaskList('1001'),领取调用 receiveTickets({ activityId:'1001', taskCode })"
},
"placeholderStyle": "min-height:58vw; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0; margin:0; padding:0",
"previewImageClass": "preview-image",
@ -971,11 +971,11 @@
- 对接抽奖区奖品池展示:`/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'`
- 补充 CollectCards 第三个任务模块 `taskModule`:任务接口和字段先参考 LuckyDollars Season5`taskList` 默认空数组,列表项使用 `itemCenter` + `taskItemBg`contentStyle 固定为 `display:flex; padding:8px; justify-content:space-between; align-items:center`,不引用 Season5 的任务类型图标和票券图标;右侧任务按钮也改为 `itemCenter` 背景图 + 翻译文字Gulben 页面中任务列表和领取任务等涉及 `activityId` 的接口统一固定传 `gulbenActivityId` 的值,也就是字符串 `'1001'`
- 补充 Personal Rank / Reward 预览结构:只展示 `RewardTop1``RewardTop2``RewardTop3` 三个 100vw 的 `itemCenter` 模块,从上往下排序;奖励数据调用 `apiGetActivityResource(rewardActivityType)`,当前 `rewardActivityType` 临时使用 `DAILY_RECHARGE_TASK2` 便于预览;接口返回 10 套奖励时只取前 3 套,每套最多 6 个奖励;如果某套只有 5 个奖励,则复制第 5 个奖励补到第 6 个位置用于预览;布局参考 DailyRecharge左侧主奖励大图右侧小奖励固定按 2/2/1 三行展示,第三行即使没有奖励也保留单展示位不可见占位并居中;奖励文案统一使用 `formatRewardDisplay(reward.type, reward)`
- 补充 Personal Rank / Ranking 预览结构:活动礼物模块使用 `giftBg` 的 3 个预设空位直接展示 giftPhoto / giftCandy不额外套参考页的 `gift` 单项背景;排行榜布局参考 LuckyDollars Season5数据改用 `/activity/week-star/ranking``/activity/week-star/my-rank`;前三名使用 GulbenFestival 私有 `components/topUser.vue` + `top1/top2/top3`,布局为第二名/第一名/第三名左中右排列且第一名稍宽;贡献值参考 WeeklyStar 使用金币图标,前三名贡献值外观在私有组件内参考 GamesKing 的 `topUserDaily.vue` 圆角渐变底;排行榜排名和贡献值文字统一使用白色;第四名以后和我的排名使用 `RankingItem` 承载,名称下方新增 vip 等级图、userLevelBg、wealthLevelBg 等级行vip 图片根据 `vipLevelName``VISCOUNT->vip1``EARL->vip2``MARQUIS->vip3``DUKEI->vip4``KING->vip5``EMPEROR->vip6` 映射展示,没有 `vipLevelName` 时不展示userLevel 背景图根据 `charmLevel` 按 1-10/11-20/21-30/31-40/41-50 对应 `userLevel1Bg~userLevel5Bg`wealthLevel 背景图根据 `wealthLevel` 按同样分段对应 `wealthLevel1Bg~wealthLevel5Bg`,内部居中展示对应等级文字,文字白色且 `font-size:1em`;切换到该标签时贴底展示我的排名,并通过底部 `rankingLoadmore` 上拉加载更多
- `Personal Rank / Reward``Personal Rank / Ranking` 的活动奖励、活动礼物都来自 `/activity/king-queen/effective?templateId=2057676469390872578`;页面内必须通过共享缓存方法读取,避免进入 Reward 和 Ranking 时重复请求同一个 effective 接口。
- 补充 Room Rank / Reward 和 RankingReward 直接 100vw 展示 `ruleActivity`Ranking 复用 Personal Rank 的排行榜展示和 weekly-star 上拉分页,但不展示活动礼物模块,头像统一 `border-radius:4px`列表和我的排名只展示名称、ID、金币数量不展示 `vip1``userLevel1Bg``wealthLevel1Bg` 三个等级图片
- 补充 Room Rank / Reward 和 RankingReward 直接 100vw 展示 `ruleActivity`Ranking 复用 Personal Rank 的排行榜展示和 weekly-star 上拉分页,但不展示活动礼物模块;前三名头像保持 `border-radius:50%`,第四名以后列表和我的排名头像使用 `border-radius:4px`列表和我的排名只展示名称、ID、金币数量不展示 `vip1``userLevel1Bg``wealthLevel1Bg` 三个等级图片
- 创建本地 OSS 目录 `public/oss/h5/Azizi/Activities/GulbenFestivalWebp/`
- 添加路由 `/activities/gulben-festival`
- 添加安全白名单和活动页权限片段
@ -1023,13 +1023,14 @@
- 页面初始化数据要仿照 `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`
- Room Rank 的 UI 仍复用当前排行榜结构,不展示活动礼物模块;前三名头像保持 `border-radius:50%`,第四名以后列表和我的排名头像保持 `border-radius:4px`列表和我的排名只展示名称、ID、金币数量不展示 `vip1``userLevel1Bg``wealthLevel1Bg`
## 本次同步:奖励池抽奖结果弹窗
- `/activity/card/reward/draw` 返回数据按 `body.rewards` 解析;兼容旧的 `body.results` 和直接数组返回,最终统一存入 `drawResult.results`
- 单抽和 5 连抽接口成功后先播放奖励池模拟转圈动画,动画结束后再展示 `resultBg` 结果弹窗。
- 结果弹窗图片优先取 `rewards` 中第一个 `isWin === true` 的奖品;如果没有中奖标记,则退回使用第一条结果;奖励池抽奖结果图宽度为 `45vw`,抽卡/合成结果图继续使用 `28vw`
- 奖励池抽奖结果弹窗在奖品和底部按钮之间保留一行居中信息:有 `days` 时展示 `reward_days` 翻译文本,没有 `days` 时直接展示 `prizeValue`5 连抽时 `1/5``2/5` 等页码在同一行内绝对定位贴右展示。
- 结果弹窗底部按钮文本为 `Confirm`,点击后关闭弹窗。
- 停点匹配优先级为 `prizeId -> prizeImage -> prizeType + prizeValue`,匹配当前奖池 `activeLotteryPrizes` 中对应奖品后,将 `lotteryPrizeActiveBg` 停在该奖品位置。
- 匹配不到接口奖品时才使用本地 `prizeIndex` 兜底;没有任何可用停点时停在当前奖池第一个奖品,避免继续使用旧测试逻辑固定停第 4 个。