(function () { var VALID_TABS = ['cp', 'brother', 'sister']; var LAYER_BASE = './assets/yumi/layers/'; var THEMES = { cp: { bg: 'bg-cp.png', tab: 'tab-cp.png', card: 'card-cp.png', center: 'center-cp.png', level: 'level-cp.png', }, brother: { bg: 'bg-brother.png', tab: 'tab-brother.png', card: 'card-brother.png', center: 'center-brother.png', level: 'level-brother.png', }, sister: { bg: 'bg-sister.png', tab: 'tab-sister.png', card: 'card-sister.png', center: 'center-sister.png', level: 'level-sister.png', }, }; var DEFAULT_RELATION = { days: 0, togetherDays: 0, currentLevel: 0, nextLevel: 0, currentExp: 0, nextExp: 0, neededExp: 0, nextExpLevel: 0, progressPercent: 0, me: { avatar: '' }, partner: { avatar: '' }, }; var page = document.getElementById('yumiCpPage'); var stage = document.querySelector('.yumi-cp-stage'); var backButton = document.getElementById('yumiBackButton'); var helpButton = document.getElementById('yumiHelpButton'); var tabButtons = Array.prototype.slice.call( document.querySelectorAll('.yumi-tab-button[data-tab]') ); var bgLayer = document.getElementById('yumiBgLayer'); var tabJewel = document.getElementById('yumiTabJewel'); var cardSkin = document.getElementById('yumiCardSkin'); var centerArt = document.getElementById('yumiCenterArt'); var levelBgs = Array.prototype.slice.call( document.querySelectorAll('.yumi-level-bg') ); var leftAvatar = document.getElementById('yumiLeftAvatar'); var rightAvatar = document.getElementById('yumiRightAvatar'); var daysValue = document.getElementById('yumiDaysValue'); var expAway = document.getElementById('yumiExpAway'); var currentLevel = document.getElementById('yumiCurrentLevel'); var nextLevel = document.getElementById('yumiNextLevel'); var progressFill = document.getElementById('yumiProgressFill'); var progressText = document.getElementById('yumiProgressText'); var timeText = document.getElementById('yumiTimeText'); var state = { activeTab: normalizeTab(readParam('tab')) || 'cp', data: clone(DEFAULT_RELATION), tabData: {}, requestSeq: 0, }; var stageResizeObserver = null; function clone(value) { return JSON.parse(JSON.stringify(value)); } function readParam(name) { var params = new URLSearchParams(window.location.search || ''); return params.get(name); } function normalizeTab(value) { var tab = String(value || '') .trim() .toLowerCase(); return VALID_TABS.indexOf(tab) >= 0 ? tab : ''; } function readNumber(value, fallback) { var number = Number(value); return Number.isFinite(number) ? number : fallback || 0; } function firstValue() { for (var i = 0; i < arguments.length; i += 1) { var value = arguments[i]; if (value !== undefined && value !== null && value !== '') return value; } return undefined; } function t(key, fallback) { if (window.HyAppI18n && window.HyAppI18n.t) { return window.HyAppI18n.t(key, fallback); } return fallback || key; } function formatNumber(value) { var number = Math.max(0, Math.floor(readNumber(value, 0))); return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function asset(name) { return LAYER_BASE + name; } function syncStageScale() { if (!stage) return; var width = stage.getBoundingClientRect().width || window.innerWidth; var scale = Math.max(0.1, width / 375); // Yumi 素材和定位都是 375x812 设计稿坐标;只同步一个缩放值,避免多处百分比换算造成错位。 stage.style.setProperty('--design-scale', String(scale)); stage.style.setProperty('--stage-height', 812 * scale + 'px'); } function bindStageScale() { syncStageScale(); if (!stage) return; if (window.ResizeObserver) { stageResizeObserver = new ResizeObserver(syncStageScale); stageResizeObserver.observe(stage); return; } // 低版本 WebView 没有 ResizeObserver,只能监听窗口变化来覆盖旋转和容器宽度变更。 window.addEventListener('resize', syncStageScale); window.addEventListener('orientationchange', syncStageScale); } function syncURL(tab) { if (!window.history || !window.history.replaceState) return; var url = new URL(window.location.href); url.searchParams.set('tab', tab); // tab 是当前视觉主题状态;replaceState 让刷新可复现,同时不把每次切换压入 App WebView 返回栈。 window.history.replaceState(null, '', url.toString()); } function setImage(node, fileName) { if (!node) return; var nextSrc = asset(fileName); if (node.getAttribute('src') !== nextSrc) node.setAttribute('src', nextSrc); } function renderAvatar(container, url) { if (!container) return; container.innerHTML = ''; if (!url) return; var image = document.createElement('img'); image.src = url; image.alt = ''; container.appendChild(image); } function normalizedRelation(raw) { var data = raw || {}; return { days: readNumber(data.days, DEFAULT_RELATION.days), togetherDays: readNumber( firstValue(data.togetherDays, data.together_days), DEFAULT_RELATION.togetherDays ), currentLevel: readNumber( firstValue(data.currentLevel, data.current_level, data.level), DEFAULT_RELATION.currentLevel ), nextLevel: readNumber( firstValue(data.nextLevel, data.next_level), DEFAULT_RELATION.nextLevel ), currentExp: readNumber( firstValue(data.currentExp, data.current_exp), DEFAULT_RELATION.currentExp ), nextExp: readNumber( firstValue(data.nextExp, data.next_exp), DEFAULT_RELATION.nextExp ), neededExp: readNumber( firstValue(data.neededExp, data.needed_exp), DEFAULT_RELATION.neededExp ), nextExpLevel: readNumber( firstValue(data.nextExpLevel, data.next_exp_level), DEFAULT_RELATION.nextExpLevel ), progressPercent: Math.max( 0, Math.min( 100, readNumber( firstValue(data.progressPercent, data.progress_percent), DEFAULT_RELATION.progressPercent ) ) ), me: { avatar: String( (data.me && data.me.avatar) || data.me_avatar || '' ), }, partner: { avatar: String( (data.partner && data.partner.avatar) || data.partner_avatar || '' ), }, }; } function renderCopy() { var data = state.data; daysValue.textContent = formatNumber(data.days); expAway.textContent = t( 'cpSpace.expAway', '{exp} Exp away from Lv. {level}' ) .replace('{exp}', formatNumber(data.neededExp)) .replace('{level}', formatNumber(data.nextExpLevel)); currentLevel.textContent = t( 'cpSpace.levelValue', 'Lv.{level}' ).replace('{level}', formatNumber(data.currentLevel)); nextLevel.textContent = t('cpSpace.levelValue', 'Lv.{level}').replace( '{level}', formatNumber(data.nextLevel) ); progressText.textContent = [ formatNumber(data.currentExp), formatNumber(data.nextExp), ].join('/'); timeText.textContent = t( 'cpSpace.timeTogether', 'Time spent together: {days} days' ).replace('{days}', formatNumber(data.togetherDays)); progressFill.style.width = Math.max(1, Math.min(233, (data.progressPercent / 100) * 233)) + 'px'; renderAvatar(leftAvatar, data.me.avatar); renderAvatar(rightAvatar, data.partner.avatar); } function setActiveTab(tab, options) { var nextTab = normalizeTab(tab) || 'cp'; var theme = THEMES[nextTab]; state.activeTab = nextTab; page.setAttribute('data-tab', nextTab); setImage(bgLayer, theme.bg); setImage(tabJewel, theme.tab); setImage(cardSkin, theme.card); setImage(centerArt, theme.center); levelBgs.forEach(function (node) { setImage(node, theme.level); }); tabButtons.forEach(function (button) { var selected = button.getAttribute('data-tab') === nextTab; button.setAttribute('aria-selected', selected ? 'true' : 'false'); }); renderCopy(); if (!options || options.syncURL !== false) syncURL(nextTab); } function applyExternalData(payload) { var data = payload || {}; var tabData = data[state.activeTab] || data.relation || data; state.data = normalizedRelation(tabData); state.tabData[state.activeTab] = state.data; renderCopy(); } function showTabData(tab) { state.data = clone(state.tabData[tab] || DEFAULT_RELATION); renderCopy(); } function loadTabData(tab) { var targetTab = normalizeTab(tab) || 'cp'; if (!window.YumiCommon || !window.YumiCommon.cpSpace) { return Promise.resolve(); } var seq = (state.requestSeq += 1); // 数据由 Java 的 CP 关系接口按 relationType 拉取;seq 防止用户快速切 tab 时旧响应覆盖新主题。 return window.YumiCommon.cpSpace .loadRelation({ tab: targetTab, userId: readParam('userId') || readParam('user_id') || '', cpUserId: readParam('cpUserId') || readParam('cp_user_id') || '', }) .then(function (data) { if (seq !== state.requestSeq) return; state.tabData[targetTab] = normalizedRelation(data); if (state.activeTab === targetTab) showTabData(targetTab); }) .catch(function (error) { if (seq !== state.requestSeq) return; // WebView 首屏不能因为 token、网关或关系为空白屏;失败保留当前可交互页面,问题交给日志定位。 if (window.console && console.warn) { console.warn('Yumi CP relation load failed:', error); } }); } function bindEvents() { tabButtons.forEach(function (button) { button.addEventListener('click', function () { var tab = button.getAttribute('data-tab'); setActiveTab(tab); showTabData(state.activeTab); loadTabData(state.activeTab); }); }); if (backButton) { backButton.addEventListener('click', function () { if (window.YumiCommon && window.YumiCommon.back) { window.YumiCommon.back(); return; } if (window.HyAppBridge && window.HyAppBridge.back) { window.HyAppBridge.back(); return; } // 无客户端 bridge 的浏览器验收环境仍需保留真实返回热区。 window.history.back(); }); } if (helpButton) { helpButton.addEventListener('click', function () { if (window.HyAppBridge && window.HyAppBridge.post) { window.HyAppBridge.post('cpSpaceHelpTap', { page: 'yumi-cp-space', }); } }); } window.addEventListener('hyapp:i18n-ready', renderCopy); window.addEventListener('hyapp:cp-space-yumi-data', function (event) { applyExternalData(event.detail || {}); }); } bindStageScale(); bindEvents(); if (window.HyAppCPSpaceYumiData) applyExternalData(window.HyAppCPSpaceYumiData); setActiveTab(state.activeTab, { syncURL: false }); showTabData(state.activeTab); loadTabData(state.activeTab); if (window.HyAppBridge && window.HyAppBridge.ready) { // ready 只声明页面能力;真实关系数据由 YumiCommon 接口层拉取,客户端仍可通过事件覆盖。 window.HyAppBridge.ready({ page: 'yumi-cp-space' }); } })();