(function () { var ASSET_BASE = './assets/other/'; var COMMON_FALLBACK = { loading: 'Loading', rule: 'rule', rewards: 'Rewards', balance: 'Balance', noDate: 'No more data', balanceNotEnough: 'Insufficient balance', rewardUnavailable: 'Rewards are not available yet', requestFailed: 'Request failed', }; var WHEEL_FALLBACK = { classic: 'Classic', inLove: 'luxury', advanced: 'Advanced', historyRecord: 'History Record', rewardTips: 'Congratulations on receiving: A precious gift worth {} gold coins! Already put in the backpack', drawAgain: 'Draw again', userAward: 'Congratulations to {1} for receiving the {2} award', rule1: '1. Select a wheel and draw with coins.', rule2: '2. Rewards are issued to the backpack after a successful draw.', rule3: '3. This activity has no relationship with Apple or Google.', }; var TABS = [ { key: 'classic', labelKey: 'classic' }, { key: 'luxury', labelKey: 'inLove' }, { key: 'advanced', labelKey: 'advanced' }, ]; var CONFIGS = { classic: { key: 'classic', amountKey: 'luckyBoxDrawAmount', enabledKey: 'luckyBoxEnabled', pageSize: 10, type: 'grid', gridClass: 'grid-classic', stageClass: 'wheel-classic', lightPrefix: 'lucky_box_', minSpeed: 2000, slotCount: 8, sequence: [1, 2, 3, 5, 8, 7, 6, 4, 1, 2, 3, 5, 8, 7, 6, 4, 1], }, luxury: { key: 'luxury', amountKey: 'middleLuckyBoxDrawAmount', enabledKey: 'middleLuckyBoxEnabled', pageSize: 20, type: 'round', gridClass: 'grid-luxury', stageClass: 'wheel-luxury', lightPrefix: 'lucky_box_tow_', hasMarqueeBox: true, }, advanced: { key: 'advanced', amountKey: 'advancedLuckyBoxDrawAmount', enabledKey: 'advancedLuckyBoxEnabled', pageSize: 10, type: 'grid', gridClass: 'grid-advanced', stageClass: 'wheel-advanced', minSpeed: 1000, hasMarqueeBox: true, slotCount: 12, sequence: [1, 2, 3, 4, 6, 8, 12, 11, 10, 9, 7, 5, 1], }, }; function query() { var params = new URLSearchParams(window.location.search || ''); var hash = window.location.hash || ''; if (hash.indexOf('?') >= 0) { new URLSearchParams(hash.split('?').slice(1).join('?')).forEach( function (value, key) { if (!params.has(key)) params.set(key, value); } ); } return params; } function readQuery(name) { return query().get(name) || ''; } function toast(message) { if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.alert(message); } function stateFor() { return { rewards: [], gridItems: [], activeIndex: null, isSpinning: false, spinInterval: null, spinSpeed: 50, targetIndex: null, goldBtn: [ { name: '1 Time', value: 1 }, { name: '10 Time', value: 10 }, { name: '50 Time', value: 50 }, ], activeBtnIndex: 1, showRule: false, showRecord: false, showReward: false, currentReward: { rewardValue: 0, rewards: [] }, drawAmount: 0, historyRecord: [], roomId: '', loading: false, finished: true, pageNo: 1, isInit: false, pageLoading: false, drawFailed: false, currentBackground: 'light', backgroundInterval: null, rotateDeg: 0, rotationTimer: null, }; } function envelopeData(response) { if (response && Object.prototype.hasOwnProperty.call(response, 'data')) return response.data; return response; } function firstPresentValue(source, keys) { source = source || {}; for (var index = 0; index < keys.length; index += 1) { var value = source[keys[index]]; if (value !== undefined && value !== null && value !== '') return value; } return ''; } function normalizeCompareValue(value) { return String(value || '') .trim() .toLowerCase(); } function resolveAssetUrl(url) { var value = String(url || '').trim(); if (!value) return ''; if ( /^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value) || value.indexOf('/') === 0 || value.indexOf('./') === 0 || value.indexOf('../') === 0 ) return value; return ASSET_BASE + value; } function isDynamicAssetUrl(url) { var clean = String(url || '') .split('?')[0] .split('#')[0]; return /\.(svga|pag|mp4|webm|mov)$/i.test(clean); } function positiveModulo(value, size) { return ((value % size) + size) % size; } function roundPrizeAngle(index, count) { var sectorAngle = 360 / Math.max(1, count || 1); // 奖品图标本身按这个角度排布,指针停止也必须使用同一个基准,否则会停在扇区中间而不是奖品上。 return index * sectorAngle + sectorAngle / 50; } function normalizeImageObject(source, keys, fallbackKey) { var target = Object.assign({}, source || {}); var image = firstPresentValue(target, keys); if (image) target[fallbackKey] = resolveAssetUrl(image); return target; } function normalizeGridItem(item) { var target = Object.assign({}, item || {}); var merchandise = normalizeImageObject( target.merchandise || target, [ 'coverUrl', 'cover', 'resourceCover', 'avatar', 'icon', 'imageUrl', ], 'coverUrl' ); target.merchandise = merchandise; return target; } function parseJSON(value) { if (!value || typeof value !== 'string') return {}; try { return JSON.parse(value); } catch (error) { return {}; } } function normalizeWheelReward(item) { var target = Object.assign({}, item || {}); var metadata = parseJSON( firstPresentValue(target, ['metadataJson', 'metadata_json']) ); var rewardType = firstPresentValue(target, [ 'rewardType', 'reward_type', ]); var metadataResourceId = firstPresentValue(metadata, [ 'resource_id', 'resourceId', ]); var rewardId = firstPresentValue(target, [ 'rewardId', 'reward_id', 'resourceId', 'resource_id', ]) || metadataResourceId; var metadataAssetUrl = firstPresentValue(metadata, [ 'asset_url', 'assetUrl', ]); var previewUrl = firstPresentValue(metadata, ['preview_url', 'previewUrl']) || (isDynamicAssetUrl(metadataAssetUrl) ? '' : metadataAssetUrl) || firstPresentValue(target, [ 'coverUrl', 'cover', 'resourceCover', 'avatar', 'icon', 'imageUrl', 'previewUrl', 'preview_url', ]); var animationUrl = firstPresentValue(metadata, [ 'animation_url', 'animationUrl', ]); if ( !isDynamicAssetUrl(animationUrl) && isDynamicAssetUrl(metadataAssetUrl) ) { animationUrl = metadataAssetUrl; } var displayName = firstPresentValue(target, [ 'displayName', 'display_name', 'name', 'rewardName', ]); var rewardCoins = Number( firstPresentValue(target, [ 'rewardCoins', 'reward_coins', 'value', ]) || 0 ); var displayValue = Number( firstPresentValue(target, [ 'displayValue', 'display_value', 'rewardValue', 'reward_value', 'value', 'rewardCoins', 'reward_coins', ]) || 0 ); var rewardCount = Number( firstPresentValue(target, [ 'rewardCount', 'reward_count', 'count', ]) || 1 ); target.id = firstPresentValue(target, ['id', 'tierId', 'tier_id']) || rewardId || displayName; target.tierId = firstPresentValue(target, ['tierId', 'tier_id']); target.rewardId = rewardId; target.rewardConfigId = firstPresentValue(target, [ 'rewardConfigId', 'reward_config_id', ]); target.resourceId = firstPresentValue(target, ['resourceId', 'resource_id']) || metadataResourceId; target.rewardType = rewardType; target.value = displayValue || rewardCoins || rewardCount; target.displayGoldAmount = target.value; target.rewardCount = rewardCount; target.count = rewardCount; target.name = displayName; target.coverUrl = resolveAssetUrl(previewUrl); target.animationUrl = rewardType === 'coin' ? '' : resolveAssetUrl(animationUrl); target.merchandise = { name: displayName, coverUrl: target.coverUrl, animationUrl: target.animationUrl, }; return normalizeGridItem(target); } function normalizeWheelRewards(data) { data = data || {}; var items = Array.isArray(data) ? data : data.rewards || data.tiers || (data.config && data.config.tiers); return Array.isArray(items) ? items.map(normalizeWheelReward).map(normalizeGridItem) : []; } function visibleWheelRewards(data, config) { var items = normalizeWheelRewards(data); var slotCount = Number(config && config.slotCount); // H5 格子数量是固定视觉契约:classic 只有 8 个外圈奖品位,center 是抽奖按钮。 // 如果旧配置或缓存仍返回第 9 个奖品,前端不渲染多余格子,避免按钮被奖品占位或抽奖动画索引错位。 return slotCount > 0 ? items.slice(0, slotCount) : items; } function normalizeRewardItem(item) { var target = normalizeImageObject( item, [ 'coverUrl', 'cover', 'resourceCover', 'avatar', 'icon', 'imageUrl', 'previewUrl', 'preview_url', ], 'coverUrl' ); var metadata = parseJSON( firstPresentValue(target, ['metadataJson', 'metadata_json']) ); var assetUrl = firstPresentValue(metadata, ['asset_url', 'assetUrl']) || firstPresentValue(target, ['assetUrl', 'asset_url']); if (isDynamicAssetUrl(target.coverUrl)) target.coverUrl = ''; if (!target.coverUrl && assetUrl && !isDynamicAssetUrl(assetUrl)) { target.coverUrl = resolveAssetUrl(assetUrl); } var animationUrl = firstPresentValue(metadata, ['animation_url', 'animationUrl']) || firstPresentValue(target, ['animationUrl', 'animation_url']); if (!isDynamicAssetUrl(animationUrl) && isDynamicAssetUrl(assetUrl)) { animationUrl = assetUrl; } target.animationUrl = isDynamicAssetUrl(animationUrl) ? resolveAssetUrl(animationUrl) : ''; return target; } function rewardCountValue(item) { var value = Number( firstPresentValue(item, ['count', 'rewardCount', 'reward_count']) || 0 ); return value > 0 ? value : 1; } function rewardGroupKey(item) { item = item || {}; var metadata = parseJSON( firstPresentValue(item, ['metadataJson', 'metadata_json']) ); var metadataResourceId = firstPresentValue(metadata, [ 'resource_id', 'resourceId', ]); return [ firstPresentValue(item, [ 'resourceId', 'resource_id', 'rewardId', 'reward_id', 'id', ]) || metadataResourceId, firstPresentValue(item, [ 'rewardType', 'reward_type', 'resourceType', 'resource_type', ]) || firstPresentValue(metadata, ['resource_type', 'resourceType']), firstPresentValue(item, ['animationUrl', 'animation_url']), firstPresentValue(item, [ 'coverUrl', 'cover_url', 'previewUrl', 'preview_url', 'cover', ]), firstPresentValue(item, [ 'name', 'displayName', 'display_name', 'rewardName', 'reward_name', ]), rewardDisplayValue(item), ] .map(function (value) { return normalizeCompareValue(value); }) .join('|'); } function aggregateRewardItems(items) { var map = {}; var list = []; (items || []).forEach(function (item) { var key = rewardGroupKey(item); var count = rewardCountValue(item); if (!map[key]) { map[key] = Object.assign({}, item, { count: 0, rewardCount: 0, }); list.push(map[key]); } map[key].count += count; map[key].rewardCount = map[key].count; }); return list; } function normalizeHistoryRecord(record) { var target = Object.assign({}, record || {}); var rewards = Array.isArray(target.rewards) ? target.rewards : []; target.rewards = rewards.map(normalizeRewardItem); return target; } function normalizeHintItem(item) { return normalizeImageObject( item, [ 'avatar', 'coverUrl', 'cover', 'resourceCover', 'icon', 'imageUrl', ], 'avatar' ); } function prizePreviewSource(item) { item = item || {}; var merchandise = item.merchandise || {}; var assetUrl = firstPresentValue(merchandise, ['assetUrl', 'asset_url']) || firstPresentValue(item, ['assetUrl', 'asset_url']); var animationUrl = firstPresentValue(merchandise, ['animationUrl', 'animation_url']) || firstPresentValue(item, ['animationUrl', 'animation_url']); if (!isDynamicAssetUrl(animationUrl) && isDynamicAssetUrl(assetUrl)) { animationUrl = assetUrl; } var coverUrl = firstPresentValue(merchandise, [ 'coverUrl', 'cover', 'previewUrl', 'preview_url', ]) || firstPresentValue(item, [ 'coverUrl', 'cover', 'resourceCover', 'previewUrl', 'preview_url', 'avatar', 'icon', 'imageUrl', ]) || (isDynamicAssetUrl(assetUrl) ? '' : assetUrl); animationUrl = resolveAssetUrl(animationUrl); return { animationUrl: isDynamicAssetUrl(animationUrl) ? animationUrl : '', coverUrl: resolveAssetUrl(coverUrl), }; } function normalizeDrawResult(data) { var target = Object.assign({ rewardValue: 0, rewards: [] }, data || {}); var rewards = Array.isArray(target.rewards) ? target.rewards : []; target.rawRewards = rewards.slice(); if ( rewards.length === 0 && firstPresentValue(target, [ 'selectedTierId', 'selected_tier_id', 'rewardType', 'reward_type', 'rewardId', 'reward_id', ]) ) { rewards = [normalizeWheelReward(target)]; target.rawRewards = [Object.assign({}, data || {})]; } target.rewards = rewards .map(normalizeWheelReward) .map(normalizeRewardItem); target.rewards = aggregateRewardItems(target.rewards); if (target.rewards.length > 0) { var aggregateValue = target.rewards.reduce(function (sum, item) { return sum + rewardDisplayValue(item) * rewardCountValue(item); }, 0); if (aggregateValue > 0) target.rewardValue = aggregateValue; } return target; } function normalizeWalletBalance(payload) { var data = envelopeData(payload) || {}; var list = Array.isArray(data) ? data : data.balances || data.values || data.list || []; var info = Array.isArray(list) ? list.find(function (item) { var type = firstPresentValue(item, [ 'asset_type', 'assetType', 'walletType', 'type', ]); return String(type || '').toUpperCase() === 'COIN'; }) : null; return info ? Number( firstPresentValue(info, [ 'available_amount', 'availableAmount', 'balance', 'amount', ]) || 0 ) : 0; } function createCommandID(prefix) { var random = ''; if (window.crypto && window.crypto.getRandomValues) { var values = new Uint32Array(2); window.crypto.getRandomValues(values); random = values[0].toString(36) + values[1].toString(36); } else { random = Math.random().toString(36).slice(2); } return ( String(prefix || 'wheel') + '_' + Date.now().toString(36) + '_' + random ); } function firstDrawReward(drawResult) { drawResult = drawResult || {}; if ( Array.isArray(drawResult.rawRewards) && drawResult.rawRewards.length > 0 ) return drawResult.rawRewards[0]; if (Array.isArray(drawResult.records) && drawResult.records.length > 0) return drawResult.records[0]; if (Array.isArray(drawResult.rewards) && drawResult.rewards.length > 0) return drawResult.rewards[0]; return null; } function rewardDisplayValue(reward) { return Number( firstPresentValue(reward, [ 'value', 'displayGoldAmount', 'goldAmount', 'rewardValue', 'amount', ]) || 0 ); } function gridItemDisplayValue(item) { return Number( firstPresentValue(item, [ 'value', 'displayGoldAmount', 'goldAmount', ]) || 0 ); } function resolveRewardTargetIndex(gridItems, drawResult) { var reward = firstDrawReward(drawResult); if (!reward || !Array.isArray(gridItems) || gridItems.length === 0) return 0; var rewardMetadata = parseJSON( firstPresentValue(reward, ['metadataJson', 'metadata_json']) ); var metadataResourceId = firstPresentValue(rewardMetadata, [ 'resource_id', 'resourceId', ]); var rewardId = normalizeCompareValue( firstPresentValue(reward, [ 'id', 'tierId', 'tier_id', 'selectedTierId', 'selected_tier_id', 'rewardId', 'reward_id', 'rewardConfigId', 'reward_config_id', 'resourceId', 'resource_id', ]) || metadataResourceId ); var rewardName = normalizeCompareValue( firstPresentValue(reward, [ 'name', 'resourceName', 'resource_name', 'rewardName', 'reward_name', ]) || firstPresentValue(rewardMetadata, [ 'resource_name', 'resourceName', 'resource_code', 'resourceCode', ]) ); var rewardCover = normalizeCompareValue( firstPresentValue(reward, [ 'coverUrl', 'cover_url', 'cover', 'resourceCover', 'resource_cover', 'previewUrl', 'preview_url', ]) || firstPresentValue(rewardMetadata, [ 'preview_url', 'previewUrl', 'asset_url', 'assetUrl', ]) ); var rewardValue = rewardDisplayValue(reward); function matchBy(predicate) { for (var index = 0; index < gridItems.length; index += 1) { if (predicate(gridItems[index] || {})) return index; } return -1; } var byId = rewardId ? matchBy(function (item) { return ( normalizeCompareValue( firstPresentValue(item, [ 'id', 'rewardId', 'reward_id', 'rewardConfigId', 'reward_config_id', 'resourceId', 'resource_id', ]) ) === rewardId ); }) : -1; if (byId >= 0) return byId; var byCoverValue = rewardCover ? matchBy(function (item) { var merchandise = item.merchandise || {}; return ( normalizeCompareValue( firstPresentValue(merchandise, ['coverUrl', 'cover']) ) === rewardCover && (!rewardValue || gridItemDisplayValue(item) === rewardValue) ); }) : -1; if (byCoverValue >= 0) return byCoverValue; var byNameValue = rewardName ? matchBy(function (item) { var merchandise = item.merchandise || {}; return ( normalizeCompareValue( firstPresentValue(merchandise, [ 'name', 'resourceName', 'resource_name', 'rewardName', 'reward_name', ]) ) === rewardName && (!rewardValue || gridItemDisplayValue(item) === rewardValue) ); }) : -1; if (byNameValue >= 0) return byNameValue; var byValue = rewardValue ? matchBy(function (item) { return gridItemDisplayValue(item) === rewardValue; }) : -1; return byValue >= 0 ? byValue : 0; } new Vue({ el: '#app', data: function () { return { tabs: TABS, currentLanguage: 'en', currentPage: 'classic', userBalance: 0, pageLoading: true, enabled: { classic: true, luxury: true, advanced: true, }, preview: { open: false, coverUrl: '', animationUrl: '', animationReady: false, }, pages: { classic: stateFor(), luxury: stateFor(), advanced: stateFor(), }, }; }, computed: { active: function () { return this.pages[this.currentPage]; }, currentConfig: function () { return CONFIGS[this.currentPage]; }, pageClass: function () { return this.currentConfig.stageClass; }, durationSpeed: function () { var count = this.active.rewards.length || 1; return 10 * count + 's'; }, marqueeRewards: function () { return this.active.rewards.concat(this.active.rewards); }, balanceEnough: function () { return ( this.userBalance >= this.active.goldBtn[this.active.activeBtnIndex].value * this.active.drawAmount ); }, }, created: function () { this.currentLanguage = this.resolveLanguage(); document.documentElement.lang = this.currentLanguage; document.documentElement.dir = this.currentLanguage === 'ar' ? 'rtl' : 'ltr'; this.currentPage = this.firstPageFromURL(); this.initRoomID(); this.loadData(); }, beforeDestroy: function () { this.closePrizePreview(); if (this.previewPlayer && this.previewPlayer.destroy) { this.previewPlayer.destroy(); } Object.keys(this.pages).forEach( function (key) { this.stopTimers(this.pages[key]); }.bind(this) ); }, methods: { resolveLanguage: function () { if (window.HyAppI18n && window.HyAppI18n.lang) return window.HyAppI18n.lang(); return readQuery('appLanguage') || readQuery('lang') || 'en'; }, t: function (key) { var fullKey = 'wheel.' + key; var fallback = WHEEL_FALLBACK[key] || fullKey; return window.HyAppI18n && window.HyAppI18n.t ? window.HyAppI18n.t(fullKey, fallback) : fallback; }, common: function (key) { var fullKey = 'wheel.common.' + key; var fallback = COMMON_FALLBACK[key] || key; return window.HyAppI18n && window.HyAppI18n.t ? window.HyAppI18n.t(fullKey, fallback) : fallback; }, replaceOne: function (key, value) { return this.t(key).replace(/{}/g, value); }, replaceTwo: function (key, first, second) { return this.t(key) .replace(/\{1}/g, first) .replace(/\{2}/g, second); }, asset: function (name) { return ASSET_BASE + name; }, ensurePreviewPlayer: function () { var target = this.$refs.previewEffect; if (!target || !window.HyAppEffectPlayer) return null; if ( this.previewPlayer && this.previewPlayer.container === target ) return this.previewPlayer; if (this.previewPlayer && this.previewPlayer.destroy) { this.previewPlayer.destroy(); } this.previewPlayer = window.HyAppEffectPlayer.create(target, { activeClass: 'is-active', fit: 'contain', loop: true, muted: true, svgaScript: '../../common/vendor/svga.min.js', waitForWindowLoad: false, }); return this.previewPlayer; }, openPrizePreview: function (item) { var source = prizePreviewSource(item); if (!source.animationUrl && !source.coverUrl) return; this.closePrizePreview(); this.preview = { open: true, coverUrl: source.coverUrl, animationUrl: source.animationUrl, animationReady: false, }; // 动态资源优先,但必须保留静态图兜底;播放器加载失败时用户仍能看到预览图,不出现空黑层。 this.$nextTick(function () { if (!this.preview.animationUrl) return; var player = this.ensurePreviewPlayer(); if (!player) return; var animationUrl = this.preview.animationUrl; player .play(animationUrl, { svgaScript: '../../common/vendor/svga.min.js', waitForWindowLoad: false, }) .then( function () { if ( this.preview.open && this.preview.animationUrl === animationUrl ) { this.preview.animationReady = true; } }.bind(this) ) .catch( function () { if ( this.preview.open && this.preview.animationUrl === animationUrl ) { this.preview.animationReady = false; } }.bind(this) ); }); }, closePrizePreview: function () { if (this.previewPlayer) this.previewPlayer.stop(); this.preview = { open: false, coverUrl: '', animationUrl: '', animationReady: false, }; }, firstPageFromURL: function () { var page = readQuery('wheel') || readQuery('tab') || ''; var hash = window.location.hash || ''; if (page === 'middle') page = 'luxury'; if (!page && hash.indexOf('advanced') !== -1) page = 'advanced'; if (!page && hash.indexOf('middle') !== -1) page = 'luxury'; return CONFIGS[page] ? page : 'classic'; }, initRoomID: function () { var roomId = readQuery('roomId') || readQuery('room_id') || ''; Object.keys(this.pages).forEach( function (key) { this.pages[key].roomId = roomId; }.bind(this) ); }, wheelAPI: function () { if (!window.HyAppAPI || !window.HyAppAPI.activityWheel) { return null; } return window.HyAppAPI.activityWheel; }, loadData: function () { this.pageLoading = true; return this.getUserWalletInfo() .then( function () { this.pageLoading = false; this.loadPageData(this.currentPage); }.bind(this) ) .catch( function () { // 钱包接口依赖登录态,失败时不能让活动页白屏;后续抽奖仍会由余额判断和服务端扣费共同拦截。 this.pageLoading = false; this.loadPageData(this.currentPage); }.bind(this) ); }, getUserWalletInfo: function () { if (!window.HyAppAPI || !window.HyAppAPI.wallet) return Promise.reject(new Error('HyAppAPI unavailable')); return window.HyAppAPI.wallet.balances('COIN').then( function (res) { this.userBalance = normalizeWalletBalance(res); }.bind(this) ); }, loadPageData: function (page) { var state = this.pages[page]; var config = CONFIGS[page]; this.configHandle(state, config); this.recordsHandle(state, config); this.userRecordsHandle(state, config); }, rewardsListHandle: function (state, config) { var api = this.wheelAPI(); if (!api) return Promise.reject(new Error('HyAppAPI unavailable')); return api .config(config.key) .then( function (res) { state.isInit = true; var data = envelopeData(res) || {}; var drawPrice = Number( firstPresentValue(data, [ 'drawPriceCoins', 'draw_price_coins', 'drawPrice', 'draw_price', ]) || 0 ); if (drawPrice > 0) state.drawAmount = drawPrice; this.enabled[config.key] = firstPresentValue(data, [ 'enabled', config.enabledKey, ]) !== false; // 后端配置来自资源组:金币只要求预览图字段为空也能展示金额,礼物/道具/装扮则统一读取 metadata 的预览图和动态图。 state.gridItems = this.enabled[config.key] ? visibleWheelRewards(data, config) : []; }.bind(this) ) .catch(function (error) { state.isInit = true; state.gridItems = []; console.warn('wheel rewards load failed', error); }); }, drawHandle: function (state, config) { var api = this.wheelAPI(); if (!api) { state.drawFailed = true; this.stopSpinning(state); toast(this.common('requestFailed')); return Promise.resolve(); } return api .draw(config.key, { command_id: createCommandID(config.key), amount: state.goldBtn[state.activeBtnIndex].value * state.drawAmount, times: state.goldBtn[state.activeBtnIndex].value, roomId: state.roomId, }) .then( function (res) { var data = normalizeDrawResult(envelopeData(res)); state.drawFailed = false; if (data.coin_balance_after !== undefined) { this.userBalance = Number( data.coin_balance_after || 0 ); } else { this.getUserWalletInfo(); } this.recordsHandle(state, config); state.currentReward = data; state.targetIndex = resolveRewardTargetIndex( state.gridItems, data ); }.bind(this) ) .catch( function (error) { state.drawFailed = true; this.stopSpinning(state); toast( error.message || this.common('requestFailed') ); }.bind(this) ); }, recordsHandle: function (state, config) { var api = this.wheelAPI(); if (!api) return Promise.resolve(); state.loading = true; if (state.isInit) state.pageNo += 1; return api .history(config.key, { pageNo: state.pageNo, pageSize: config.pageSize, }) .then(function (res) { var data = envelopeData(res) || {}; var records = Array.isArray(data.records) ? data.records : []; if (records.length === 0) state.finished = true; state.loading = false; // 历史记录和中奖弹窗使用同一批奖品字段,统一标准化后再追加,分页加载不会把裸文件名带进 DOM。 state.historyRecord = state.historyRecord.concat( records.map(normalizeHistoryRecord) ); }) .catch(function () { state.loading = false; state.finished = true; }); }, userRecordsHandle: function (state, config) { var api = this.wheelAPI(); if (!api) return Promise.resolve(); return api .hints(config.key) .then(function (res) { var data = envelopeData(res) || {}; if (Array.isArray(data.values)) state.rewards = data.values.map(normalizeHintItem); }) .catch(function () { state.rewards = []; }); }, configHandle: function (state, config) { return this.rewardsListHandle(state, config).then( function () { if (!this.enabled[this.currentPage]) this.handleSelect(this.firstEnabledPage()); }.bind(this) ); }, firstEnabledPage: function () { for (var index = 0; index < TABS.length; index += 1) { if (this.enabled[TABS[index].key]) return TABS[index].key; } return 'classic'; }, handleGoldBtn: function (index) { this.active.activeBtnIndex = index; }, handleSelect: function (tab) { if (!CONFIGS[tab] || !this.enabled[tab]) return; this.currentPage = tab; if (!this.pages[tab].gridItems.length) this.loadPageData(tab); }, startLottery: function () { var state = this.active; var config = this.currentConfig; if (!state.gridItems.length || state.drawAmount <= 0) { toast(this.common('rewardUnavailable')); return; } if (!this.balanceEnough) { toast(this.common('balanceNotEnough')); return; } if (state.isSpinning) return; state.showReward = false; state.drawFailed = false; state.isInit = false; state.finished = false; state.pageNo = 1; state.historyRecord = []; state.targetIndex = null; state.activeIndex = null; if (config.type === 'round') this.backgroundChange(state); this.startSpinning(state, config); this.drawHandle(state, config); this.stopShowReward(state, config); }, startSpinning: function (state, config) { if (config.type === 'round') { this.startRoundSpinning(state); return; } state.isSpinning = true; var sequenceIndex = 0; var spinCount = 0; var maxFastSpins = 25; var speedIncrement = 100; var minSpeed = config.minSpeed; state.spinInterval = window.setInterval( function () { state.activeIndex = config.sequence[sequenceIndex] - 1; sequenceIndex = (sequenceIndex + 1) % config.sequence.length; spinCount += 1; if (config.key === 'classic') { state.currentBackground = state.currentBackground === 'light' ? 'light2' : 'light'; } if ( spinCount > maxFastSpins && state.targetIndex !== null ) { state.spinSpeed += speedIncrement; if ( Math.abs( state.activeIndex - state.targetIndex ) <= 4 ) state.spinSpeed += speedIncrement * 2; if ( state.activeIndex === state.targetIndex && state.spinSpeed >= minSpeed ) this.stopSpinning(state); } }.bind(this), state.spinSpeed ); }, startRoundSpinning: function (state) { var startWait = Date.now(); var waitStartDeg = state.rotateDeg; state.isSpinning = true; if (state.rotationTimer) window.cancelAnimationFrame(state.rotationTimer); var animateToTarget = function () { var count = Math.max(1, state.gridItems.length || 1); var targetIndex = Number(state.targetIndex || 0); var duration = 4200; var startTime = Date.now(); var startDeg = state.rotateDeg; var currentDeg = positiveModulo(startDeg, 360); var targetBaseDeg = roundPrizeAngle(targetIndex, count); var forwardDelta = positiveModulo( targetBaseDeg - currentDeg, 360 ); // 指针转盘必须等接口给出真实 targetIndex 后,从当前角度继续正向缓停到奖品图标角度;不能归零或使用假结果。 var targetDeg = startDeg + 360 * 6 + forwardDelta; var animate = function () { var elapsed = Date.now() - startTime; var progress = Math.min(1, elapsed / duration); var easedProgress = this.roundStopEase(progress); state.rotateDeg = startDeg + (targetDeg - startDeg) * easedProgress; if (progress >= 1) { state.isSpinning = false; state.rotateDeg = targetDeg; state.rotationTimer = null; this.showDrawReward(state); return; } state.rotationTimer = window.requestAnimationFrame(animate); }.bind(this); animate(); }.bind(this); var waitForTarget = function () { if ( state.targetIndex !== null && state.targetIndex !== undefined ) { animateToTarget(); return; } state.rotateDeg = waitStartDeg + ((Date.now() - startWait) / 1000) * 720; state.rotationTimer = window.requestAnimationFrame(waitForTarget); }; waitForTarget(); }, customEase: function (value) { return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 3) / 2; }, roundStopEase: function (value) { return 1 - Math.pow(1 - value, 4); }, backgroundChange: function (state) { state.backgroundInterval = window.setInterval(function () { state.currentBackground = state.currentBackground === 'light' ? 'light2' : 'light'; }, 100); }, showDrawReward: function (state) { if (state.drawFailed || state.targetIndex === null) return; if ( state.targetIndex !== null && state.targetIndex !== undefined && typeof state.activeIndex === 'number' ) { state.activeIndex = state.targetIndex; } state.showReward = true; state.isInit = true; window.clearInterval(state.backgroundInterval); }, stopShowReward: function (state, config) { if (config && config.type === 'round') return; window.setTimeout( function () { this.showDrawReward(state); }.bind(this), 3300 ); }, stopSpinning: function (state) { this.stopTimers(state); state.isSpinning = false; state.spinSpeed = 50; state.activeIndex = state.targetIndex !== null && state.targetIndex !== undefined ? state.targetIndex : null; }, stopTimers: function (state) { window.clearInterval(state.spinInterval); window.clearInterval(state.backgroundInterval); if (state.rotationTimer) window.cancelAnimationFrame(state.rotationTimer); state.spinInterval = null; state.rotationTimer = null; }, getPrizePosition: function (index) { var count = Math.max(1, this.active.gridItems.length || 1); var radius = 150; var center = { x: 50, y: 50 }; var itemAngle = roundPrizeAngle(index, count); var angle = (itemAngle * Math.PI) / 180; var distance = radius * 0.25; var x = center.x + Math.sin(angle) * distance; var y = center.y - Math.cos(angle) * distance; var textRotation = itemAngle; return { left: x + '%', top: y + '%', transform: 'translate(-50%, -50%) rotate(' + textRotation + 'deg)', }; }, buttonStyle: function (index) { if (this.currentPage !== 'luxury') return {}; var name = 'lucky_box_tow_' + (index === this.active.activeBtnIndex ? '' : 'un') + 'btn' + (index + 1) + '.png'; return { backgroundImage: 'url(' + this.asset(name) + ')' }; }, pointerStyle: function () { if (this.currentPage !== 'luxury') return {}; return { '--pointer-rotate': this.active.rotateDeg + 'deg' }; }, formatDate: function (time, format) { if (!time) return ''; var date = new Date( Number(time) || String(time).replace(/-/g, '/') ); function zero(value) { return (value < 10 ? '0' : '') + value; } return (format || 'yyyy-MM-dd HH:mm:ss').replace( /yyyy|MM|dd|HH|mm|ss/g, function (token) { if (token === 'yyyy') return zero(date.getFullYear()); if (token === 'MM') return zero(date.getMonth() + 1); if (token === 'dd') return zero(date.getDate()); if (token === 'HH') return zero(date.getHours()); if (token === 'mm') return zero(date.getMinutes()); return zero(date.getSeconds()); } ); }, }, }); })();