hyapp-h5/activity/wheel/script.js
2026-06-24 13:47:47 +08:00

1413 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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',
totalRewards: 'total rewards:',
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) {
var metadata = parseJSON(
firstPresentValue(reward, ['metadataJson', 'metadata_json'])
);
return Number(
firstPresentValue(reward, [
'value',
'displayValue',
'display_value',
'displayGoldAmount',
'goldAmount',
'rewardValue',
'reward_value',
'rewardCoins',
'reward_coins',
'amount',
]) ||
firstPresentValue(metadata, [
'display_value',
'displayValue',
'coin_price',
'coinPrice',
'price',
'value',
]) ||
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;
},
rewardTotalValue: function (drawResult) {
drawResult = drawResult || {};
var directValue = Number(
firstPresentValue(drawResult, [
'rewardValue',
'reward_value',
'totalValue',
'total_value',
]) || 0
);
if (directValue > 0) return directValue;
// 弹窗展示必须以当前聚合奖品为准:多抽会先合并相同资源,兜底重算可覆盖旧接口未返回总价值的情况。
return (drawResult.rewards || []).reduce(function (sum, item) {
return (
sum + rewardDisplayValue(item) * rewardCountValue(item)
);
}, 0);
},
formatRewardValue: function (value) {
var number = Number(value || 0);
return Number.isFinite(number)
? Math.floor(number).toLocaleString('en-US')
: '0';
},
rewardTotalText: function (drawResult) {
return this.formatRewardValue(
this.rewardTotalValue(drawResult)
);
},
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());
}
);
},
},
});
})();