(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,避免移动端 // 页面被无限放大,同时 320~480px 的常见设备宽度都会真正铺满视口。 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(); } })();