hyapp-h5/vip/fami.js
2026-07-17 11:54:43 +08:00

1010 lines
37 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 () {
'use strict';
var DESIGN_WIDTH = 375;
var MAX_LAYOUT_WIDTH = 480;
var OUTFIT_INITIAL_SCROLL = 53;
var VERSION_RETRY_LIMIT = 1;
var FALLBACK_OUTFIT = './assets/fami/layers/outfit-fallback.png';
var UNKNOWN_ICON = './assets/fami/icons/unknown-report.svg';
var ICON_FILES = {
vip_badge_identity: './assets/fami/icons/award-1.svg',
vip_medal: './assets/fami/icons/award-2.svg',
avatar_frame: './assets/fami/icons/avatar-frame.svg',
vip_title: './assets/fami/icons/username.svg',
chat_bubble: './assets/fami/icons/chat.svg',
vip_gift: './assets/fami/icons/gift.svg',
entry_effect: './assets/fami/icons/entry.svg',
visitor_history: './assets/fami/icons/profile-circle-1.svg',
voice_wave: './assets/fami/icons/radio.svg',
profile_card: './assets/fami/icons/profile-card.svg',
custom_room_background: './assets/fami/icons/profile-circle-2.svg',
room_image_message: './assets/fami/icons/file-gif.svg',
animated_avatar: './assets/fami/icons/file-gif.svg',
animated_room_cover: './assets/fami/icons/file-gif.svg',
mic_skin: './assets/fami/icons/microphone.svg',
room_border: './assets/fami/icons/shield-1.svg',
vehicle: './assets/fami/icons/vehicle.svg',
colored_room_name: './assets/fami/icons/courier-1.svg',
colored_nickname: './assets/fami/icons/courier-2.svg',
colored_id: './assets/fami/icons/id-currency.svg',
gift_tray_skin: './assets/fami/icons/gift.svg',
hide_profile_data: './assets/fami/icons/hidden.svg',
custom_avatar_frame: './assets/fami/icons/avatar-frame.svg',
leaderboard_invisible: './assets/fami/icons/hidden.svg',
daily_coin_rebate: './assets/fami/icons/currency.svg',
custom_profile_card: './assets/fami/icons/profile-card.svg',
anonymous_profile_visit: './assets/fami/icons/anonymous.svg',
custom_gift: './assets/fami/icons/gift.svg',
room_entry_notice: './assets/fami/icons/entry.svg',
custom_pretty_id: './assets/fami/icons/pretty-id.svg',
custom_vehicle: './assets/fami/icons/vehicle.svg',
anti_kick: './assets/fami/icons/anti-kick.svg',
anti_mute: './assets/fami/icons/shield-3.svg',
online_global_notice: './assets/fami/icons/notice.svg',
unknown: UNKNOWN_ICON,
};
var state = {
packages: [],
selectedLevel: 0,
vipState: emptyVipState(),
programConfig: {},
pageState: 'loading',
purchasing: false,
settingsSaving: false,
stickyNavigationActive: false,
loadError: null,
lockedScrollTop: 0,
lastFocused: null,
mediaPlayer: null,
};
function byId(id) {
return document.getElementById(id);
}
function text(value) {
return String(value === undefined || value === null ? '' : value);
}
function number(value) {
var result = Number(value || 0);
return Number.isFinite(result) ? result : 0;
}
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback || key);
}
return fallback || key;
}
function interpolate(template, values) {
return text(template).replace(/\{([a-z_]+)\}/gi, function (_, key) {
return Object.prototype.hasOwnProperty.call(values, key)
? text(values[key])
: '';
});
}
function locale() {
return window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en';
}
function emptyVipState() {
return {
paid_vip: {},
effective_vip: {},
effective_source: 'none',
effective_benefits: [],
user_settings: {
room_entry_notice_enabled: true,
online_global_notice_enabled: true,
},
program_config: {},
};
}
function ensureFamiTenant() {
var url = new URL(window.location.href);
if (url.searchParams.get('app_code') !== 'fami') {
// common/api.js 只信任 URL 中显式 app_code 作为 X-App-Code避免跨页面缓存串租户。
// Fami 专属入口因此在任何请求发出前把租户写入当前 URL而不是仅修改 localStorage。
url.searchParams.set('app_code', 'fami');
window.history.replaceState(null, '', url.toString());
}
if (window.HyAppAPI && window.HyAppAPI.setAppCode) {
window.HyAppAPI.setAppCode('fami');
}
}
function syncViewport() {
var mount = byId('famiVipMount');
var page = byId('famiVipPage');
if (!mount || !page) return;
var viewportWidth = Math.max(1, window.innerWidth || DESIGN_WIDTH);
var viewportHeight = Math.max(
560,
window.innerHeight || document.documentElement.clientHeight || 812
);
// 页面仍以 375px 设计坐标绘制,手机宽度整体等比适配,确保所有独立图层之间的
// 几何关系不会因零散百分比换算而漂移;只在桌面预览时限制到 480px避免移动端
// 页面被无限放大,同时 320480px 的常见设备宽度都会真正铺满视口。
var layoutWidth = Math.min(MAX_LAYOUT_WIDTH, viewportWidth);
var scale = layoutWidth / DESIGN_WIDTH;
mount.style.width = layoutWidth + 'px';
mount.style.height = viewportHeight + 'px';
page.style.height = viewportHeight / scale + 'px';
page.style.transform = 'scale(' + scale + ')';
syncStickyNavigation();
}
function syncStickyNavigation() {
var scroll = byId('pageScroll');
var navigation = byId('stickyNavigation');
if (!scroll || !navigation) return;
// 毛玻璃只在导航真正抵达滚动容器顶部后启用;初始 48px 状态栏区域仍展示
// 原设计背景,避免页面刚开始轻微滚动时就出现一块悬浮的深色玻璃层。
var threshold = Math.max(0, navigation.offsetTop - 0.5);
var active = scroll.scrollTop >= threshold;
if (active === state.stickyNavigationActive) return;
state.stickyNavigationActive = active;
navigation.classList.toggle('is-stuck', active);
}
function packageList(payload) {
return Array.isArray(payload && payload.packages)
? payload.packages.slice().sort(function (a, b) {
return (
number(a.sort_order) - number(b.sort_order) ||
number(a.level) - number(b.level)
);
})
: [];
}
function normalizeVipState(payload) {
var source = payload && typeof payload === 'object' ? payload : {};
var normalized = emptyVipState();
normalized.paid_vip = source.paid_vip || {};
normalized.equipped_trial_card = source.equipped_trial_card || null;
normalized.effective_vip = source.effective_vip || {};
normalized.effective_source = text(source.effective_source || 'none');
normalized.effective_benefits = Array.isArray(source.effective_benefits)
? source.effective_benefits.slice()
: [];
normalized.evaluated_at_ms = number(source.evaluated_at_ms);
normalized.program_config = source.program_config || {};
normalized.user_settings = Object.assign(
{},
normalized.user_settings,
source.user_settings || {}
);
return normalized;
}
function configVersion(payload) {
var config =
(payload && payload.program_config) ||
(payload && payload.state && payload.state.program_config) ||
{};
return number(config.config_version);
}
function versionsMatch(packagesPayload, mePayload) {
var packagesVersion = configVersion(packagesPayload);
var meVersion = configVersion(mePayload);
// 旧服务滚动升级时可能仍返回 0只有双方都声明了版本才能判定冲突。
// 一旦双方都有版本,绝不提交跨版本 state/package 组合,避免按钮资格与权益列表互相矛盾。
return !packagesVersion || !meVersion || packagesVersion === meVersion;
}
function selectedPackage() {
return (
state.packages.find(function (item) {
return number(item.level) === number(state.selectedLevel);
}) || null
);
}
function chooseInitialLevel(previousLevel) {
if (!state.packages.length) return 0;
var requested = number(
new URL(window.location.href).searchParams.get('level')
);
var effective = state.vipState.effective_vip || {};
var candidates = [previousLevel, requested];
if (effective.active) candidates.push(number(effective.level));
for (var index = 0; index < candidates.length; index += 1) {
var candidate = number(candidates[index]);
if (
candidate &&
state.packages.some(function (item) {
return number(item.level) === candidate;
})
) {
return candidate;
}
}
var purchasable = state.packages.find(function (item) {
return item.can_purchase === true;
});
return number((purchasable || state.packages[0]).level);
}
function commitResponses(packagesPayload, mePayload) {
var previousLevel = state.selectedLevel;
state.packages = packageList(packagesPayload);
// /vip/me 是当前身份的专用读模型packages 中的 state 仅作为同版本购买列表快照。
// 资格集合统一采用 me.state客户端不按等级或 unlock_level 自行补齐。
state.vipState = normalizeVipState(
(mePayload && mePayload.state) ||
(packagesPayload && packagesPayload.state)
);
state.programConfig =
(packagesPayload && packagesPayload.program_config) ||
(mePayload && mePayload.program_config) ||
state.vipState.program_config ||
{};
state.selectedLevel = chooseInitialLevel(previousLevel);
state.loadError = null;
state.pageState = 'ready';
renderAll();
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({
page: 'fami_vip',
config_version: number(state.programConfig.config_version),
});
}
}
function loadData(attempt) {
attempt = number(attempt);
if (!window.HyAppAPI || !window.HyAppAPI.vip) {
return Promise.reject(new Error('vip_api_unavailable'));
}
// 两个请求并发缩短首屏时间;结果在版本校验通过前不会进入可见 state
// 任意一支失败都保留上次完整画面,避免出现“新套餐 + 旧会员”的半更新状态。
return Promise.all([
window.HyAppAPI.vip.packages(),
window.HyAppAPI.vip.me(),
]).then(function (responses) {
if (!versionsMatch(responses[0], responses[1])) {
if (attempt < VERSION_RETRY_LIMIT) {
return loadData(attempt + 1);
}
var mismatch = new Error('vip_config_version_mismatch');
mismatch.code = 'vip_config_version_mismatch';
throw mismatch;
}
commitResponses(responses[0] || {}, responses[1] || {});
});
}
function setPageState(nextState, message) {
state.pageState = nextState;
var page = byId('famiVipPage');
var loadMessage = byId('loadMessage');
var retry = byId('retryButton');
if (page) {
page.setAttribute('data-page-state', nextState);
page.setAttribute(
'aria-busy',
nextState === 'loading' ? 'true' : 'false'
);
}
if (loadMessage && message) loadMessage.textContent = message;
if (retry) retry.hidden = nextState !== 'error';
}
function hydrate() {
setPageState('loading', t('fami_vip.loading', 'Loading...'));
return loadData(0).catch(function (error) {
state.loadError = error;
setPageState(
'error',
error && error.code === 'vip_config_version_mismatch'
? t(
'fami_vip.version_changed',
'VIP configuration changed. Please retry.'
)
: t('fami_vip.load_failed', 'Unable to load VIP data.')
);
});
}
function renderAll() {
renderLevels();
renderOutfits();
renderPrivileges();
renderBottomBar();
if (state.pageState === 'ready') setPageState('ready');
}
function renderLevels() {
var root = byId('levelTabs');
if (!root) return;
root.replaceChildren();
var track = document.createElement('div');
track.className = 'fami-vip__level-track';
track.setAttribute('role', 'tablist');
state.packages.forEach(function (item) {
var level = number(item.level);
var selected = level === number(state.selectedLevel);
var button = document.createElement('button');
button.type = 'button';
button.className = 'fami-vip__level-button';
button.dataset.level = text(level);
button.dataset.testid = 'vip-level-' + level;
button.setAttribute('role', 'tab');
button.setAttribute('aria-selected', selected ? 'true' : 'false');
button.setAttribute('tabindex', selected ? '0' : '-1');
var label = document.createElement('span');
label.textContent = text(item.name || 'VIP' + level);
button.appendChild(label);
track.appendChild(button);
});
root.appendChild(track);
window.requestAnimationFrame(scrollSelectedLevelIntoView);
}
function scrollSelectedLevelIntoView() {
var root = byId('levelTabs');
if (!root) return;
var selected = root.querySelector('[aria-selected="true"]');
if (!selected) return;
var target = Math.max(
0,
selected.offsetLeft - (root.clientWidth - selected.clientWidth) / 2
);
root.scrollLeft = target;
}
function rewardItems(item) {
return Array.isArray(item && item.reward_items)
? item.reward_items.slice()
: [];
}
function rewardPreview(item) {
return text(
item &&
(item.preview_url ||
item.cover_url ||
item.asset_url ||
item.animation_url)
);
}
function renderOutfits() {
var list = byId('outfitList');
var titleNode = byId('outfitTitle');
if (!list || !titleNode) return;
var items = rewardItems(selectedPackage());
titleNode.textContent =
t('fami_vip.exclusive_outfit', 'Exclusive Outfit') +
'(' +
items.length +
'/' +
items.length +
')';
list.replaceChildren();
if (!items.length) {
var empty = document.createElement('p');
empty.className = 'fami-vip__outfit-empty';
empty.textContent = t(
'fami_vip.no_outfits',
'No exclusive outfits for this level.'
);
list.appendChild(empty);
return;
}
items.forEach(function (item, index) {
var card = document.createElement('button');
card.type = 'button';
card.className = 'fami-vip__outfit-card';
card.dataset.outfitIndex = text(index);
card.dataset.testid = 'vip-outfit-card';
card.setAttribute(
'aria-label',
text(item.name || t('fami_vip.outfit', 'VIP outfit'))
);
var skin = document.createElement('img');
skin.className = 'fami-vip__outfit-skin';
skin.src = './assets/fami/layers/outfit-card-glow.png';
skin.alt = '';
skin.setAttribute('aria-hidden', 'true');
var media = document.createElement('img');
media.className = 'fami-vip__outfit-media';
media.alt = '';
media.loading = index > 2 ? 'lazy' : 'eager';
media.src = rewardPreview(item) || FALLBACK_OUTFIT;
media.addEventListener('error', function () {
if (!media.src.endsWith(FALLBACK_OUTFIT.replace('./', ''))) {
media.src = FALLBACK_OUTFIT;
}
});
var name = document.createElement('span');
name.className = 'fami-vip__outfit-name';
name.textContent = text(
item.name || t('fami_vip.outfit', 'VIP outfit')
);
card.appendChild(skin);
card.appendChild(media);
card.appendChild(name);
list.appendChild(card);
});
window.requestAnimationFrame(function () {
var viewport = byId('outfitViewport');
if (viewport) {
viewport.scrollLeft =
list.scrollWidth > viewport.clientWidth
? OUTFIT_INITIAL_SCROLL
: 0;
}
});
}
function benefitItems(item) {
return Array.isArray(item && item.benefits)
? item.benefits.slice().sort(function (a, b) {
return (
number(a.sort_order) - number(b.sort_order) ||
text(a.benefit_code).localeCompare(text(b.benefit_code))
);
})
: [];
}
function effectiveBenefitCodes() {
var codes = Object.create(null);
(state.vipState.effective_benefits || []).forEach(function (item) {
var code = text(item && item.benefit_code);
if (code) codes[code] = true;
});
return codes;
}
function iconFile(benefit) {
var iconKey = text(
benefit && (benefit.icon_key || benefit.benefit_code)
).toLowerCase();
return ICON_FILES[iconKey] || UNKNOWN_ICON;
}
function renderPrivileges() {
var grid = byId('privilegeGrid');
var titleNode = byId('privilegeTitle');
if (!grid || !titleNode) return;
var items = benefitItems(selectedPackage());
var unlockedCodes = effectiveBenefitCodes();
var unlockedCount = items.filter(function (item) {
return !!unlockedCodes[text(item.benefit_code)];
}).length;
titleNode.textContent =
t('fami_vip.vip_privileges', 'VIP Privileges') +
'(' +
unlockedCount +
'/' +
items.length +
')';
grid.replaceChildren();
if (!items.length) {
var empty = document.createElement('p');
empty.className = 'fami-vip__privilege-empty';
empty.textContent = t(
'fami_vip.no_privileges',
'No privileges for this level.'
);
grid.appendChild(empty);
return;
}
items.forEach(function (item) {
var code = text(item.benefit_code);
var unlocked = !!unlockedCodes[code];
var button = document.createElement('button');
button.type = 'button';
button.className =
'fami-vip__privilege-item' +
(unlocked ? ' is-unlocked' : ' is-locked');
button.dataset.benefitCode = code;
button.dataset.testid = 'vip-privilege-item';
button.setAttribute(
'aria-label',
text(item.name || code) +
', ' +
(unlocked
? t('fami_vip.unlocked', 'Unlocked')
: t('fami_vip.locked', 'Locked'))
);
var icon = document.createElement('span');
icon.className = 'fami-vip__privilege-icon';
var image = document.createElement('img');
image.src = iconFile(item);
image.alt = '';
image.setAttribute('aria-hidden', 'true');
image.addEventListener(
'error',
function () {
image.src = UNKNOWN_ICON;
},
{ once: true }
);
icon.appendChild(image);
var name = document.createElement('span');
name.className = 'fami-vip__privilege-name';
name.textContent = text(item.name || code);
button.appendChild(icon);
button.appendChild(name);
grid.appendChild(button);
});
}
function formatCoin(value) {
try {
return new Intl.NumberFormat(locale()).format(number(value));
} catch (_) {
return text(number(value));
}
}
function formatDuration(durationMS) {
var days = Math.max(0, Math.round(number(durationMS) / 86400000));
return interpolate(t('fami_vip.days', '{count} Days'), {
count: formatCoin(days),
});
}
function formatExpiry(timestampMS) {
if (!number(timestampMS)) return t('fami_vip.no_expiry', 'No expiry');
try {
return new Intl.DateTimeFormat(locale(), {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(number(timestampMS)));
} catch (_) {
return new Date(number(timestampMS)).toLocaleString();
}
}
function sameEffectiveLevel(item) {
var vip = state.vipState.effective_vip || {};
return !!vip.active && number(vip.level) === number(item && item.level);
}
function paidVip() {
return state.vipState.paid_vip || {};
}
function purchaseLabel(item) {
var paid = paidVip();
if (paid.active && number(paid.level) === number(item.level)) {
return t('fami_vip.renew_vip', 'Renew VIP');
}
if (paid.active && number(paid.level) < number(item.level)) {
return t('fami_vip.upgrade_vip', 'Upgrade VIP');
}
return t('fami_vip.activate_vip', 'Activate VIP');
}
function renderBottomBar() {
var item = selectedPackage();
var priceState = byId('priceState');
var activeState = byId('activeState');
var priceText = byId('priceText');
var activeLevelText = byId('activeLevelText');
var expiryText = byId('expiryText');
var purchaseButton = byId('purchaseButton');
var purchaseText = byId('purchaseText');
if (!purchaseButton || !purchaseText) return;
if (!item) {
priceState.hidden = false;
activeState.hidden = true;
priceText.textContent = '--';
purchaseText.textContent = t('fami_vip.unavailable', 'Unavailable');
purchaseButton.disabled = true;
return;
}
var showActive = sameEffectiveLevel(item);
priceState.hidden = showActive;
activeState.hidden = !showActive;
if (showActive) {
var effective = state.vipState.effective_vip || {};
var source = text(state.vipState.effective_source);
activeLevelText.textContent = interpolate(
source === 'trial'
? t('fami_vip.trial_level', 'Trial Level: {level}')
: t('fami_vip.unlocked_level', 'Unlocked Level: {level}'),
{
level: text(
effective.name || item.name || 'VIP' + item.level
),
}
);
expiryText.textContent = interpolate(
t('fami_vip.valid_until', 'Valid until: {date}'),
{ date: formatExpiry(effective.expires_at_ms) }
);
} else {
priceText.textContent =
formatCoin(item.price_coin) +
' / ' +
formatDuration(item.duration_ms);
}
var canPurchase = item.can_purchase === true;
purchaseText.textContent = state.purchasing
? t('fami_vip.processing', 'Processing...')
: canPurchase
? purchaseLabel(item)
: t('fami_vip.unavailable', 'Unavailable');
purchaseButton.disabled =
state.pageState !== 'ready' || state.purchasing || !canPurchase;
purchaseButton.setAttribute(
'aria-label',
canPurchase
? purchaseLabel(item)
: lockedReason(item.purchase_locked_reason)
);
}
function lockedReason(reason) {
if (text(reason) === 'current_vip_higher') {
return t(
'fami_vip.current_vip_higher',
'A lower VIP level cannot be purchased while a higher level is active.'
);
}
return t('fami_vip.unavailable', 'Unavailable');
}
function commandID(level) {
var random = '';
if (window.crypto && window.crypto.getRandomValues) {
var bytes = new Uint32Array(2);
window.crypto.getRandomValues(bytes);
random = bytes[0].toString(36) + bytes[1].toString(36);
} else {
random = Math.random().toString(36).slice(2);
}
return (
'fami_vip_' +
number(level) +
'_' +
Date.now().toString(36) +
'_' +
random
).slice(0, 128);
}
function purchaseSelected() {
var item = selectedPackage();
if (
!item ||
item.can_purchase !== true ||
state.purchasing ||
!window.HyAppAPI ||
!window.HyAppAPI.vip
) {
if (item && item.purchase_locked_reason) {
toast(lockedReason(item.purchase_locked_reason));
}
return;
}
state.purchasing = true;
renderBottomBar();
var previousState = state.vipState;
window.HyAppAPI.vip
.purchase({
command_id: commandID(item.level),
level: number(item.level),
})
.then(function (response) {
if (!response || !response.state) {
throw new Error('vip_purchase_state_missing');
}
// PurchaseVip 的 state 是扣费、会员和权益在同一事务后的完整回包。
// 只有完整 state 存在才一次性替换,绝不先改等级再等待第二个请求补权益。
state.vipState = normalizeVipState(response.state);
state.programConfig =
response.program_config ||
state.vipState.program_config ||
state.programConfig;
renderAll();
toast(t('fami_vip.purchase_success', 'Purchase successful.'));
// 购买响应没有更新后的套餐 can_purchase后台刷新失败时仍保留已原子提交的新 state
// 但不会把失败刷新产生的半成品覆盖到页面。
return loadData(0).catch(function () {
return undefined;
});
})
.catch(function (error) {
state.vipState = previousState;
toast(
(error && error.message) ||
t('fami_vip.purchase_failed', 'Purchase failed.')
);
})
.finally(function () {
state.purchasing = false;
renderAll();
});
}
function toast(message) {
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
}
}
function openModal(id) {
var modal = byId(id);
var scroll = byId('pageScroll');
if (!modal || !scroll) return;
state.lastFocused = document.activeElement;
state.lockedScrollTop = scroll.scrollTop;
scroll.classList.add('is-locked');
modal.hidden = false;
var focusTarget = modal.querySelector('.fami-vip__dialog-close');
if (focusTarget) focusTarget.focus();
}
function closeModal(id) {
var modal = byId(id);
var scroll = byId('pageScroll');
if (!modal || !scroll) return;
modal.hidden = true;
if (id === 'mediaModal') destroyMediaPlayer();
if (!document.querySelector('.fami-vip__modal:not([hidden])')) {
scroll.classList.remove('is-locked');
scroll.scrollTop = state.lockedScrollTop;
}
if (state.lastFocused && state.lastFocused.focus) {
state.lastFocused.focus();
}
}
function showBenefit(code) {
var item = benefitItems(selectedPackage()).find(function (benefit) {
return text(benefit.benefit_code) === text(code);
});
if (!item) return;
var unlocked = !!effectiveBenefitCodes()[text(item.benefit_code)];
var iconRoot = byId('detailIcon');
var icon = document.createElement('img');
icon.src = iconFile(item);
icon.alt = '';
icon.setAttribute('aria-hidden', 'true');
iconRoot.replaceChildren(icon);
byId('detailTitle').textContent = text(item.name || item.benefit_code);
var description = text(item.description).trim();
byId('detailDescription').textContent = description;
byId('detailDescription').hidden = !description;
byId('detailState').textContent = unlocked
? t('fami_vip.unlocked', 'Unlocked')
: t('fami_vip.locked', 'Locked');
openModal('detailModal');
}
function showSettings() {
var settings = state.vipState.user_settings || {};
byId('roomEntrySetting').checked =
settings.room_entry_notice_enabled !== false;
byId('onlineNoticeSetting').checked =
settings.online_global_notice_enabled !== false;
openModal('settingsModal');
}
function updateSetting(field, checked) {
if (
state.settingsSaving ||
!window.HyAppAPI ||
!window.HyAppAPI.vip ||
!window.HyAppAPI.vip.updateSettings
) {
renderSettingsInputs();
return;
}
var previous = Object.assign({}, state.vipState.user_settings || {});
var payload = {};
payload[field] = !!checked;
state.settingsSaving = true;
setSettingsDisabled(true);
window.HyAppAPI.vip
.updateSettings(payload)
.then(function (response) {
if (!response || !response.settings) {
throw new Error('vip_settings_missing');
}
state.vipState.user_settings = Object.assign(
{},
previous,
response.settings
);
renderSettingsInputs();
toast(t('fami_vip.settings_saved', 'Settings saved.'));
})
.catch(function () {
// PATCH 失败必须回滚当前开关,不能让 H5 显示一个服务端并未接受的权限偏好。
state.vipState.user_settings = previous;
renderSettingsInputs();
toast(
t('fami_vip.settings_failed', 'Unable to save settings.')
);
})
.finally(function () {
state.settingsSaving = false;
setSettingsDisabled(false);
});
}
function renderSettingsInputs() {
var settings = state.vipState.user_settings || {};
byId('roomEntrySetting').checked =
settings.room_entry_notice_enabled !== false;
byId('onlineNoticeSetting').checked =
settings.online_global_notice_enabled !== false;
}
function setSettingsDisabled(disabled) {
byId('roomEntrySetting').disabled = disabled;
byId('onlineNoticeSetting').disabled = disabled;
}
function destroyMediaPlayer() {
if (state.mediaPlayer && state.mediaPlayer.destroy) {
state.mediaPlayer.destroy();
}
state.mediaPlayer = null;
var stage = byId('mediaStage');
if (stage) stage.replaceChildren();
}
function appendMediaImage(stage, source, name) {
var image = document.createElement('img');
image.src = source || FALLBACK_OUTFIT;
image.alt = text(name);
image.addEventListener(
'error',
function () {
image.src = FALLBACK_OUTFIT;
},
{ once: true }
);
stage.replaceChildren(image);
}
function showOutfit(index) {
var item = rewardItems(selectedPackage())[number(index)];
if (!item) return;
destroyMediaPlayer();
var stage = byId('mediaStage');
var title = text(item.name || t('fami_vip.outfit', 'VIP outfit'));
byId('mediaTitle').textContent = title;
var animationURL = text(item.animation_url);
var fallback = rewardPreview(item) || FALLBACK_OUTFIT;
openModal('mediaModal');
if (
animationURL &&
window.HyAppEffectPlayer &&
window.HyAppEffectPlayer.create &&
window.HyAppEffectPlayer.inferType(animationURL)
) {
state.mediaPlayer = window.HyAppEffectPlayer.create(stage, {
fit: 'contain',
waitForWindowLoad: false,
});
state.mediaPlayer.play(animationURL).catch(function () {
destroyMediaPlayer();
appendMediaImage(stage, fallback, title);
});
return;
}
appendMediaImage(stage, fallback, title);
}
function handleClick(event) {
var levelButton = event.target.closest('[data-level]');
if (levelButton) {
var nextLevel = number(levelButton.dataset.level);
if (nextLevel && nextLevel !== state.selectedLevel) {
state.selectedLevel = nextLevel;
renderAll();
}
return;
}
var benefit = event.target.closest('[data-benefit-code]');
if (benefit) {
showBenefit(benefit.dataset.benefitCode);
return;
}
var outfit = event.target.closest('[data-outfit-index]');
if (outfit) {
showOutfit(outfit.dataset.outfitIndex);
return;
}
var close = event.target.closest('[data-modal-close]');
if (close) closeModal(close.dataset.modalClose);
}
function bindEvents() {
document.addEventListener('click', handleClick);
byId('pageScroll').addEventListener('scroll', syncStickyNavigation, {
passive: true,
});
byId('purchaseButton').addEventListener('click', purchaseSelected);
byId('retryButton').addEventListener('click', hydrate);
byId('settingsButton').addEventListener('click', showSettings);
byId('helpButton').addEventListener('click', function () {
openModal('helpModal');
});
byId('backButton').addEventListener('click', function () {
if (window.HyAppBridge && window.HyAppBridge.back) {
window.HyAppBridge.back();
} else if (window.history.length > 1) {
window.history.back();
}
});
byId('roomEntrySetting').addEventListener('change', function (event) {
updateSetting('room_entry_notice_enabled', event.target.checked);
});
byId('onlineNoticeSetting').addEventListener(
'change',
function (event) {
updateSetting(
'online_global_notice_enabled',
event.target.checked
);
}
);
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
var modal = document.querySelector(
'.fami-vip__modal:not([hidden])'
);
if (modal) closeModal(modal.id);
});
window.addEventListener('resize', syncViewport);
window.addEventListener('orientationchange', syncViewport);
window.addEventListener('hyapp:i18n-ready', renderAll);
}
function init() {
ensureFamiTenant();
syncViewport();
bindEvents();
renderAll();
// App 配置必须先确定 Fami 租户,再允许 VIP 请求发出;否则异步配置晚到时可能
// 改写请求头或 API 地址,让同一屏 packages/me 落入不同租户或环境。
var configReady =
window.HyAppAPI && window.HyAppAPI.loadAppConfig
? window.HyAppAPI.loadAppConfig('')
: Promise.resolve(null);
configReady
.catch(function () {
return null;
})
.then(hydrate);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();