feat(古尔邦节): 对接接口

This commit is contained in:
hzj 2026-05-23 17:25:18 +08:00
parent ca1cd246a0
commit 49d5345743
4 changed files with 742 additions and 91 deletions

View File

@ -1,9 +1,16 @@
import { nextTick } from 'vue'
import { imageCacheManager } from '@/utils/image/imageCacheManager'
import { isLowEndDevice } from '@/utils/performanceMode.js'
import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
import { isProtectedAssetUrl, resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
const TRANSPARENT_PLACEHOLDER =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
export const smartImage = {
beforeMount(el) {
preventNativeBrokenImage(el)
},
async mounted(el, binding) {
const skipFade = binding.modifiers.noFade || binding.arg === 'noFade' || isLowEndDevice()
const keepVisible = binding.modifiers.noFade || binding.arg === 'noFade'
@ -35,6 +42,7 @@ export const smartImage = {
}
if (currentUrl && currentUrl !== el._smartImageCurrentUrl) {
preventNativeBrokenImage(el)
prepareImageLoadingState(el, skipFade, keepVisible)
await processImage(el, currentUrl)
el._smartImageCurrentUrl = currentUrl
@ -47,21 +55,25 @@ export const smartImage = {
el.removeEventListener('error', el._smartImageErrorHandler)
delete el._smartImageCurrentUrl
delete el._smartImageOriginalBindingValue
delete el._smartImageOriginalSrcBeforePlaceholder
delete el._smartImageRequestToken
delete el._smartImageLoadHandler
delete el._smartImageErrorHandler
},
}
function getValidUrl(bindingValue, el) {
const candidates = [bindingValue, el.getAttribute('src'), el.src]
const candidates = [bindingValue, el._smartImageOriginalSrcBeforePlaceholder, el.getAttribute('src'), el.src]
for (const candidate of candidates) {
if (
typeof candidate === 'string' &&
candidate &&
candidate !== 'null' &&
candidate !== 'undefined' &&
candidate !== TRANSPARENT_PLACEHOLDER &&
!candidate.includes('blob:') &&
typeof candidate === 'string'
!candidate.includes('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw')
) {
return candidate
}
@ -70,6 +82,18 @@ function getValidUrl(bindingValue, el) {
return null
}
function preventNativeBrokenImage(el) {
const rawSrc = el.getAttribute('src') || ''
if (!rawSrc || rawSrc === TRANSPARENT_PLACEHOLDER) {
return
}
if (isProtectedAssetUrl(rawSrc)) {
el._smartImageOriginalSrcBeforePlaceholder = rawSrc
el.src = TRANSPARENT_PLACEHOLDER
}
}
function prepareImageLoadingState(el, skipFade, keepVisible = false) {
if (keepVisible) {
el.style.opacity = '1'
@ -94,8 +118,15 @@ async function processImage(el, url) {
try {
const resolvedUrl = resolveProtectedAssetUrl(url)
const requestToken = Symbol('smart-image-request')
el._smartImageRequestToken = requestToken
preventNativeBrokenImage(el)
const imageUrl = await imageCacheManager.scheduleRequest(resolvedUrl)
if (el._smartImageRequestToken !== requestToken) {
return
}
const handleLoad = () => {
showImage(el)
@ -120,7 +151,19 @@ async function processImage(el, url) {
} catch (error) {
const fallbackUrl = resolveProtectedAssetUrl(url)
console.warn(`Image processing failed: ${fallbackUrl}`, error)
el.src = fallbackUrl
const imageLoaded = await loadNativeImage(fallbackUrl)
if (imageLoaded) {
el.src = fallbackUrl
}
showImage(el)
}
}
function loadNativeImage(url) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = url
})
}

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,7 @@ export const pageConfig = {
'gameTitleSSR',
'giftBg',
'helpBt',
'historyBg',
'leftBt',
'lotteryPrizeActiveBg',
'lotteryPrizeBg',
@ -65,6 +66,7 @@ export const pageConfig = {
'redeemSR2013',
'redeemSSR2021',
'redeemSSR2022',
'resultBg',
'RewardTop1',
'RewardTop2',
'RewardTop3',

View File

@ -375,7 +375,7 @@
"barrageList": ""
},
"actions": {
"rewardDraw": "点击 lotteryBg 下层 draw1Bt 调用 apiPostCardRewardDraw({ activityId:'1001', ticketType:当前奖池 poolType, drawCount:1 }),当前奖池票券数量为 0 时按钮置灰且点击无效;点击 draw5BtBg 调用 drawCount:5当前奖池票券数量不足 5 时按钮置灰且点击无效;接口成功后刷新 apiGetCardTicketList('1001') 同步第一层三种票券数量,并保留当前 lotteryPrizeBg / lotteryPrizeActiveBg 转圈动画",
"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 卡;成功后刷新当前等级卡包和目标等级卡包",
@ -427,21 +427,21 @@
"layout": "flex-column rows + row flex center",
"gap": "R 使用 3vwSR/SSR 使用 lottery-prize-list-large 将间距缩小为 1vw",
"itemWidth": "R/SR 使用 calc((100% - var(--lottery-prize-gap) * 2) / 3)SSR 使用 calc((100% - var(--lottery-prize-gap)) / 2.2)",
"modeSwitch": "左右 leftBt 切换 lotteryR / lotterySR / lotterySSR切换时重置 activeLotteryIndex抽奖动画执行中暂不允许切换奖池",
"modeSwitch": "左右 leftBt 切换 lotteryR / lotterySR / lotterySSR切换时重置 activeLotteryIndex奖励池抽奖请求中暂不允许切换奖池",
"defaultImage": "lotteryPrizeBg",
"activeImage": "lotteryPrizeActiveBg",
"apiData": "有 /activity/card/reward/prize/list 接口数据时优先使用接口返回数组;字段为 id、poolType、prizeName、prizeType、prizeValue、prizeImage、sortOrder按 sortOrder 升序排序,并补充当前渲染 index 用于 lotteryPrizeActiveBg 高亮动画",
"itemContent": "每个奖品仍使用 itemCenter背景根据 activeLotteryIndex 切换 lotteryPrizeBg / lotteryPrizeActiveBgitemCenter 内部使用垂直 flex第一行 prizeImage 宽度 60%,第二行展示奖励信息;奖励信息使用 formatRewardDisplay(prize.prizeType, { content: prize.prizeValue, quantity: prize.prizeValue }),无结果时回退 prizeName",
"itemContent": "每个奖品仍使用 itemCenter背景根据 activeLotteryIndex 切换 lotteryPrizeBg / lotteryPrizeActiveBgitemCenter 内部使用垂直 flex第一行 prizeImage 宽度 60%,第二行展示奖励信息;奖励信息有 days 时展示 `${days} days`,没有 days 时展示 prizeValue不展示 prizeName",
"remark": "奖品区域按行渲染,每一行使用 flex 居中,避免 grid 在最后一行数量不足时贴左;没有接口数据时保留本地测试奖品占位,方便预览。"
},
"animation": {
"reference": "/activities/lucky-dollars-season5",
"mode": "切换奖品 itemCenter 的背景图来模拟转圈,而不是叠加 prizeActiveFrame 发光边框",
"mode": "当前奖励池抽奖接口成功后先播放 lotteryPrizeActiveBg 模拟转圈动画,动画结束后再展示 resultBg 结果弹窗",
"activeRule": "同一时间只有一个奖品 itemCenter 使用 lotteryPrizeActiveBg其余使用 lotteryPrizeBg",
"order": "从左到右、从上到下依次变化背景图",
"resultFlow": "点击抽奖后先请求接口,接口返回中奖奖品,再根据奖品 id/name 计算最终位置,然后启动动画并停在中奖位置",
"multiDraw": "5 连抽也只执行一次动画,最终位置取接口返回结果中的第一个中奖奖品",
"currentPreview": "requestLotteryDraw 已接入 /activity/card/reward/draw接口结果暂未映射具体中奖奖品位置时,动画仍优先停在当前奖池第 4 个奖品(数组下标 3如果当前奖池不足 4 个,则停在最后一个奖品;后续根据接口返回 prizeId/name 再补最终位置映射",
"multiDraw": "5 连抽也直接展示结果弹窗,弹窗图片取接口返回结果中的第一个中奖奖品;没有中奖标记时取第一条结果",
"currentPreview": "requestLotteryDraw 已接入 /activity/card/reward/draw接口成功后先计算中奖停点并播放模拟动画,动画结束后再展示 resultBg 结果弹窗,底部按钮文本为 Confirm点击后关闭弹窗",
"performanceNote": "当前奖品数量少,直接切换两个 itemCenter 的 imgUrl 可以满足效果;后续如果奖品数量增加、动画频率提高或低端机出现闪烁/卡顿,优先考虑改成固定默认背景 + 单个 active 覆盖层,或用 class 切换预加载后的 CSS background以减少图片 src 变更。",
"decisionRule": "只要满足效果和布局,优先选择更省性能、用户体验更稳定的方案;如果 Codex 发现更好的样式或动画实现,需要先说明方案、优缺点和影响范围,并询问是否采用,不能静默替换用户已调好的结构。"
},
@ -857,7 +857,7 @@
"topLayer": "display:flex; flex:1; align-items:center; gap:4vw",
"sideColumns": "左右两列 width:10vw; flex:0 0 10vw; 内部居中放 leftBt右侧 leftBt 水平翻转;点击左右按钮切换 lotteryR / lotterySR / lotterySSR",
"prizeGrid": "中间 flex:1使用 flex-direction:column 分行渲染,行内 flex 居中R 使用 --lottery-prize-gap:3vwSR/SSR 使用 lottery-prize-list-large 缩小到 1vw接口返回奖品按 sortOrder 排序后渲染lotteryR 默认行数 2/3/2lotterySR 3 个一行展示并放大lotterySSR 2 个一行展示并更大;默认 lotteryPrizeBg当前 activeLotteryIndex 使用 lotteryPrizeActiveBg奖品 itemCenter 内部垂直展示 60% 宽 prizeImage 和 formatRewardDisplay 生成的奖励信息",
"animation": "参考 LuckyDollars Season5 的接口返回后再开启动画逻辑;当前页面不叠加发光边框,而是通过 lotteryPrizeBg / lotteryPrizeActiveBg 背景图切换模拟转圈。点击 1 抽或 5 连抽都只执行一次动画,顺序按当前奖池渲染数组从左到右、从上到下,最终停在接口结果对应位置;当前接口结果未映射时优先停第 4 个,奖池不足 4 个时停最后一个。",
"animation": "当前奖励池抽奖播放 lotteryPrizeActiveBg 模拟转圈动画;点击 1 抽或 5 连抽接口成功后先计算中奖停点并播放动画,动画结束后再展示 resultBg 结果弹窗,底部按钮文本为 Confirm点击后关闭弹窗。",
"performanceDecision": "当前先保留直接切换 imgUrl 的实现,因为测试奖品数量少且两张图已预加载,成本可控。若后续体验不稳,优先提议改为默认背景常驻 + 单个 active 覆盖层,或 class 切 CSS background任何更优样式方案先询问用户是否采用。",
"bottomLayer": "display:flex; align-items:center; justify-content:space-around; gap:0; margin:0; padding:0draw1Bt 和 draw5BtBg 宽度 38vw两个按钮内部水平居中、垂直贴底展示 10vw 当前奖池票券图,切换 lotteryR / lotterySR / lotterySSR 时同步切换 ticketR / ticketSR / ticketSSRdraw1Bt 需要当前奖池奖励票数量 >= 1draw5BtBg 需要 >= 5不满足时置灰且不给点击点击调用 /activity/card/reward/drawticketType 使用当前奖池 poolTypedrawCount 为 1 或 5成功后刷新第一层三种票券数量"
},
@ -892,8 +892,8 @@
"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 中新增一行等级图,依次展示 vip1、itemCenter(userLevel1Bg)、itemCenter(wealthLevel1Bg),后两者宽度暂用 10vw",
"myRanking": "切换到 Personal Rank / Ranking 时固定贴底展示我的排名;当前同样使用 RankingItem 承载;排名文字和贡献值文字统一使用白色;贡献值使用金币图标,名称下方同步展示 vip1 / userLevel1Bg / wealthLevel1Bg 等级占位",
"listAfterTop3": "从第四名开始渲染列表;当前使用 Gulben 的 RankingItem 图片承载列表项;排名文字和贡献值文字统一使用白色;贡献值参考 WeeklyStar 使用金币图标 + quantity/amount不再显示 $;名称/id 的 flex-column 中新增一行等级图,vip 图片根据 rankingList 列表项 vipLevelName 按 VISCOUNT->vip1、EARL->vip2、MARQUIS->vip3、DUKEI->vip4、KING->vip5、EMPEROR->vip6 映射展示,没有 vipLevelName 时不展示userLevel 背景图根据 charmLevel 按 1-10/11-20/21-30/31-40/41-50 对应 userLevel1Bg~userLevel5BgwealthLevel 背景图根据 wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg内部居中展示对应等级文字文字白色 font-size:1em",
"myRanking": "切换到 Personal Rank / Ranking 时固定贴底展示我的排名;当前同样使用 RankingItem 承载;排名文字和贡献值文字统一使用白色;贡献值使用金币图标,名称下方同步展示 vip / userLevelBg / wealthLevelBgvip 图片根据 myRanking.vipLevelName 按 VISCOUNT->vip1、EARL->vip2、MARQUIS->vip3、DUKEI->vip4、KING->vip5、EMPEROR->vip6 映射展示,没有 vipLevelName 时不展示userLevel 背景图根据 myRanking.charmLevel 按 1-10/11-20/21-30/31-40/41-50 对应 userLevel1Bg~userLevel5BgwealthLevel 背景图根据 myRanking.wealthLevel 按同样分段对应 wealthLevel1Bg~wealthLevel5Bg内部居中展示对应等级文字文字白色 font-size:1em",
"pagination": "pageSize=10current 从 1 开始;接口返回 rankUsers.records 追加到 ranking当 current * rankUsers.size >= rankUsers.total 时停止上拉rankingLoading / rankingHasMore 防止重复请求"
},
"data": "pageConfig.activity.personalRankActivityType 默认 THIS_WEEKweekly-star 字段映射为 userNickname -> userName、quantity -> amount礼物临时使用 KingAndQueen templateId=1966020557831610370"
@ -968,12 +968,13 @@
- 补充规则弹窗:点击首行 `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%)和奖励信息;奖励信息`formatRewardDisplay(prize.prizeType, { content: prize.prizeValue, quantity: prize.prizeValue })` 生成,失败时回退 `prizeName`
- 补充抽奖动画占位逻辑:参考 `/activities/lucky-dollars-season5` 的“接口返回结果后计算最终位置再开启动画”,当前通过 `lotteryPrizeBg` / `lotteryPrizeActiveBg` 切换背景图模拟转圈5 连抽只执行一次动画
- 对接抽奖区奖品池展示:`/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` 承载,名称下方新增 `vip1``userLevel1Bg``wealthLevel1Bg` 等级行,切换到该标签时贴底展示我的排名,并通过底部 `rankingLoadmore` 上拉加载更多
- 补充 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` 三个等级图片
- 创建本地 OSS 目录 `public/oss/h5/Azizi/Activities/GulbenFestival/`
- 添加路由 `/activities/gulben-festival`
@ -1024,25 +1025,26 @@
- `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 连抽只执行一次转圈动画,优先取 `rewards` 中第一个 `isWin === true` 的奖品作为停点;如果没有中奖标记,则退回使用第一条结果。
- 停点匹配优先级为 `prizeId -> prizeName -> prizeImage -> prizeType + prizeValue`,匹配当前奖池 `activeLotteryPrizes` 中对应奖品后,将 `lotteryPrizeActiveBg` 停在该奖品位置。
- 单抽和 5 连抽接口成功后先播放奖励池模拟转圈动画,动画结束后再展示 `resultBg` 结果弹窗。
- 结果弹窗图片优先取 `rewards` 中第一个 `isWin === true` 的奖品;如果没有中奖标记,则退回使用第一条结果;奖励池抽奖结果图宽度为 `45vw`,抽卡/合成结果图继续使用 `28vw`
- 结果弹窗底部按钮文本为 `Confirm`,点击后关闭弹窗。
- 停点匹配优先级为 `prizeId -> prizeImage -> prizeType + prizeValue`,匹配当前奖池 `activeLotteryPrizes` 中对应奖品后,将 `lotteryPrizeActiveBg` 停在该奖品位置。
- 匹配不到接口奖品时才使用本地 `prizeIndex` 兜底;没有任何可用停点时停在当前奖池第一个奖品,避免继续使用旧测试逻辑固定停第 4 个。
## 本次同步:抽奖区奖励文案
- `lotteryBg` 奖品 itemCenter 内第二行奖励信息直接展示接口返回的 `prizeName`
- 该区域不再使用 `formatRewardDisplay(prize.prizeType, { content: prize.prizeValue, quantity: prize.prizeValue })` 拼接奖励文案,避免和接口配置的奖品名称不一致
- `lotteryBg` 奖品 itemCenter 内第二行奖励信息展示接口返回的 `prizeName`
- 该区域直接按接口字段展示:有 `days` 时展示 `${days} days`,没有 `days` 时展示 `prizeValue`
## 本次同步:抽奖动画防抖动
- `lotteryBg` 奖品格子的 `itemCenter` 背景固定使用 `lotteryPrizeBg`,抽奖动画不再通过切换 `:imgUrl` 改变背景图。
- `lotteryPrizeActiveBg` 改为放在奖品格子内部的绝对定位覆盖图,通过 `lottery-prize-active-bg-show` 切换 `opacity` 展示选中态。
- 由于 `lotteryPrizeActiveBg``lotteryPrizeBg` 只有颜色差异,使用覆盖图透明度切换可以避免动画过程中反复改图片 `src`,减少移动端图片重新解码、布局重算导致的页面抖动。
- 奖品图片和 `prizeName` 文案统一放到覆盖图上层,使用 `position:relative; z-index:1` 保持显示层级。
- 奖品图片和奖励信息统一放到覆盖图上层,使用 `position:relative; z-index:1` 保持显示层级。
## 本次同步:抽奖高亮防抖动方案
@ -1063,7 +1065,7 @@
- `lotteryPrizeActiveBg` 仍然用于抽奖选中态视觉,但不再作为 `itemCenter``imgUrl` 频繁切换。
- 奖品格 `itemCenter` 底图固定为 `imageUrl('lotteryPrizeBg')`
- 每个奖品格内部常驻一张绝对定位的 `lotteryPrizeActiveBg` 覆盖图class 为 `lottery-prize-active-bg`
- 当前选中奖品只切换 `lottery-prize-active-bg-show`,通过 `opacity` 展示 active 图;奖品图片和 `prizeName` 使用 `z-index:1` 保持在 active 图上层。
- 当前选中奖品只切换 `lottery-prize-active-bg-show`,通过 `opacity` 展示 active 图;奖品图片和奖励信息使用 `z-index:1` 保持在 active 图上层。
- 这样既保留 `lotteryPrizeActiveBg` 的设计效果,也避免抽奖动画中反复修改 `itemCenter` 背景图 `src` 导致页面抖动。
## 本次同步:抽奖默认选中态