(function () { var VALID_TABS = ['cp', 'brother', 'sister']; var PARTS_BASE = './assets/parts/'; var PART_NAMES = [ 'bg', 'avatar', 'heart', 'button', 'xp-track', 'xp-fill', 'level-pill', ]; var LEVEL_PARTS = {}; for (var partLevel = 1; partLevel <= 5; partLevel += 1) { LEVEL_PARTS[partLevel] = {}; PART_NAMES.forEach(function (partName) { LEVEL_PARTS[partLevel][partName] = PARTS_BASE + 'cp-lv' + partLevel + '-' + partName + '@2x.png'; }); } var page = document.getElementById('page'); var contentPanel = document.getElementById('contentPanel'); var backButton = document.getElementById('backButton'); var tabButtons = Array.prototype.slice.call( document.querySelectorAll('.tab-button[data-tab]') ); var state = { activeTab: normalizeTab(readParam('tab')) || 'cp', requestSeq: 0, cancelingId: '', tabs: { cp: createTabState(), brother: createTabState(), sister: createTabState(), }, }; function createTabState() { return { loading: false, loaded: false, error: '', relationships: [], }; } 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 normalizeLevel(value) { var level = Number(value); return Number.isFinite(level) && level >= 1 && level <= 5 ? Math.floor(level) : 1; } function readNumber(value) { var number = Number(value); return Number.isFinite(number) ? number : 0; } function readString(value) { return String(value == null ? '' : value).trim(); } function t(key, fallback) { if (window.HyAppI18n && window.HyAppI18n.t) { return window.HyAppI18n.t(key, fallback); } return fallback || key; } function escapeHTML(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function showToast(message) { if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.dispatchEvent( new CustomEvent('hyapp:toast', { detail: { message: message } }) ); } function profileFromData(data) { var json = data || {}; return { userId: readString(json.user_id || json.id), displayUserId: readString(json.display_user_id), username: readString(json.username || json.nickname || json.name), avatar: readString( json.avatar || json.avatar_url || json.avatar_path || (json.profile && json.profile.avatar) ), }; } function displayName(profile) { return ( readString(profile.username) || readString(profile.displayUserId) || readString(profile.userId) || t('cpSpace.unknownUser', 'User') ); } function daysFromFormedAt(formedAtMs) { var formedAt = readNumber(formedAtMs); if (formedAt <= 0) return 0; var now = Date.now(); if (now <= formedAt) return 1; return Math.floor((now - formedAt) / 86400000) + 1; } function currentLevelExp(item) { var total = Math.max(0, readNumber(item.intimacy_value)); var currentThreshold = readNumber(item.current_level_threshold); if (currentThreshold <= 0 || currentThreshold >= total) return total; return total - currentThreshold; } function nextLevelExp(item) { if (item.max_level) return 0; var nextThreshold = readNumber(item.next_level_threshold); var currentThreshold = readNumber(item.current_level_threshold); if (nextThreshold > 0) { if (currentThreshold > 0 && nextThreshold > currentThreshold) { return nextThreshold - currentThreshold; } return nextThreshold; } var total = Math.max(0, readNumber(item.intimacy_value)); var remaining = readNumber(item.needed_for_next_level); return remaining > 0 ? total + remaining : 0; } function progressPercent(item) { if (item.max_level) return 100; var explicit = readNumber(item.level_progress_percent); if (explicit > 0) return Math.max(0, Math.min(100, explicit)); var next = nextLevelExp(item); if (next <= 0) return 0; return Math.max(0, Math.min(100, (currentLevelExp(item) / next) * 100)); } function normalizeRelationship(item, tab) { var relationType = normalizeTab(item && item.relation_type) || tab; var level = normalizeLevel(item && item.level); var me = profileFromData(item && item.me); var partner = profileFromData(item && item.partner); var currentExp = currentLevelExp(item || {}); var nextExp = nextLevelExp(item || {}); return { relationshipId: readString(item && item.relationship_id), relationType: relationType, status: readString(item && item.status) || 'active', level: level, me: me, partner: partner, days: daysFromFormedAt(item && item.formed_at_ms), intimacyValue: Math.max(0, readNumber(item && item.intimacy_value)), currentExp: currentExp, nextExp: nextExp, progressPercent: progressPercent(item || {}), maxLevel: Boolean(item && item.max_level), breakupCostCoins: Math.max( 0, readNumber(item && item.breakup_cost_coins) ), }; } function activeRelationships(data, tab) { var list = Array.isArray(data && data.relationships) ? data.relationships : []; return list .map(function (item) { return normalizeRelationship(item || {}, tab); }) .filter(function (item) { return ( item.relationshipId && item.relationType === tab && item.status === 'active' ); }); } function setActiveTab(tab) { var nextTab = normalizeTab(tab) || 'cp'; state.activeTab = nextTab; page.setAttribute('data-tab', nextTab); tabButtons.forEach(function (button) { button.classList.toggle( 'is-active', button.getAttribute('data-tab') === nextTab ); }); render(); if (!state.tabs[nextTab].loaded && !state.tabs[nextTab].loading) { loadTab(nextTab); } } function visualLevel(item) { return item.relationType === 'cp' ? item.level : 1; } function partsFor(item) { return LEVEL_PARTS[visualLevel(item)] || LEVEL_PARTS[1]; } function nextLevelLabel(item) { if (item.maxLevel) return 'Max'; return t('cpSpace.levelValue', 'Lv.{level}').replace( '{level}', String(Math.min(5, item.level + 1)) ); } function expLabel(item) { if (item.maxLevel || item.nextExp <= 0) { return t('cpSpace.maxLevelExp', '{exp} EXP').replace( '{exp}', formatNumber(item.intimacyValue) ); } return t('cpSpace.expProgress', '{current}/{next} EXP') .replace('{current}', formatNumber(item.currentExp)) .replace('{next}', formatNumber(item.nextExp)); } function formatNumber(value) { var number = Math.max(0, Math.floor(readNumber(value))); return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function avatarHTML(profile, side, frameSrc) { var name = displayName(profile); var avatar = readString(profile.avatar); return [ '
', avatar ? [ '',
                      escapeHTML(name),
                      '', ].join('') : [ '
', escapeHTML(name.charAt(0).toUpperCase() || '?'), '
', ].join(''), '', '
', escapeHTML(name), '
', '
', ].join(''); } function cardHTML(item, index) { var parts = partsFor(item); var label = t('cpSpace.levelCard', 'Best friend level {level}').replace( '{level}', String(item.level) ); var daysText = t('cpSpace.daysValue', '{days} Days').replace( '{days}', String(item.days) ); var levelTitle = t('cpSpace.levelValue', 'Lv.{level}').replace( '{level}', String(item.level) ); var cardClass = 'relation-card relation-card--' + escapeHTML(item.relationType); if (item.relationType !== 'cp') cardClass += ' relation-card--shared'; return [ '
', '',
            escapeHTML(label),
            '', avatarHTML(item.me, 'left', parts.avatar), avatarHTML(item.partner, 'right', parts.avatar), '', '
', escapeHTML(String(item.days)), '', escapeHTML(t('cpSpace.daysUnit', 'Days')), '
', '
', escapeHTML(levelTitle), '
', '
', escapeHTML(nextLevelLabel(item)), '
', '
', escapeHTML(expLabel(item)), '
', '
', item.relationType === 'cp' ? [ '', ].join('') : '', '
', ].join(''); } function render() { var tabState = state.tabs[state.activeTab]; if (tabState.loading && !tabState.loaded) { contentPanel.innerHTML = '
' + escapeHTML(t('cpSpace.loading', 'Loading...')) + '
'; return; } if (tabState.error) { contentPanel.innerHTML = ''; return; } if (!tabState.relationships.length) { contentPanel.innerHTML = '
' + escapeHTML(t('cpSpace.empty', 'No relationship data yet.')) + '
'; return; } contentPanel.innerHTML = '
' + tabState.relationships.map(cardHTML).join('') + '
'; } function loadTab(tab) { var tabState = state.tabs[tab]; var seq = ++state.requestSeq; tabState.loading = true; tabState.error = ''; render(); // 关系事实全部从 gateway profile 接口读取;H5 只负责按 relation_type 分页拉取,不本地推断用户是否有 CP/兄弟/姐妹。 return window.HyAppAPI.cpSpace .relationships({ relation_type: tab, page: 1, page_size: tab === 'cp' ? 1 : 20, }) .then(function (data) { if (seq < state.requestSeq && tab === state.activeTab) return; tabState.relationships = activeRelationships(data, tab); tabState.loaded = true; }) .catch(function (error) { tabState.error = (error && error.message) || t('cpSpace.loadFailed', 'Load failed. Tap to retry.'); }) .then(function () { tabState.loading = false; render(); }); } function buildCommandId(relationshipId) { var random = window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : String(Date.now()) + '_' + Math.random().toString(16).slice(2); return ['h5_cp_break', relationshipId, random] .join('_') .replace(/[^A-Za-z0-9_-]/g, '_'); } function confirmBreakup(item) { var message = item.breakupCostCoins > 0 ? t( 'cpSpace.cancelConfirmWithCost', 'Canceling this CP will cost {coins} coins. Continue?' ).replace('{coins}', formatNumber(item.breakupCostCoins)) : t( 'cpSpace.cancelConfirmFree', 'Cancel this CP relationship?' ); return Promise.resolve(window.confirm(message)); } function handleCancel(index) { var item = state.tabs[state.activeTab].relationships[Number(index)] || null; if (!item || !item.relationshipId || state.cancelingId) return; confirmBreakup(item).then(function (confirmed) { if (!confirmed) return; state.cancelingId = item.relationshipId; render(); // command_id 必须由本次点击生成并随请求提交,服务端用它串联占位、钱包扣费和确认解除,网络重试不会重复扣费。 window.HyAppAPI.cpSpace .breakRelationship(item.relationshipId, { command_id: buildCommandId(item.relationshipId), }) .then(function (data) { showToast(t('cpSpace.cancelSuccess', 'Canceled.')); if (window.HyAppBridge && window.HyAppBridge.post) { window.HyAppBridge.post('cpSpaceRelationCanceled', { relationship_id: item.relationshipId, relation_type: item.relationType, breakup_cost_coins: readNumber( data && data.breakup_cost_coins ), }); } state.tabs.cp.loaded = false; return loadTab('cp'); }) .catch(function (error) { showToast( (error && error.message) || t('cpSpace.cancelFailed', 'Cancel failed.') ); }) .then(function () { state.cancelingId = ''; render(); }); }); } function applyExternalData(payload) { var data = payload || {}; VALID_TABS.forEach(function (tab) { var raw = data[tab]; if (!Array.isArray(raw)) return; state.tabs[tab].relationships = raw .map(function (item) { return normalizeRelationship(item || {}, tab); }) .filter(function (item) { return item.relationshipId; }); state.tabs[tab].loaded = true; }); if (normalizeTab(data.activeTab)) state.activeTab = data.activeTab; setActiveTab(state.activeTab); } function bindEvents() { if (backButton) { backButton.addEventListener('click', function () { if (window.HyAppBridge && window.HyAppBridge.back) { window.HyAppBridge.back(); return; } window.history.back(); }); } tabButtons.forEach(function (button) { button.addEventListener('click', function () { setActiveTab(button.getAttribute('data-tab')); }); }); contentPanel.addEventListener('click', function (event) { var cancelTarget = event.target.closest('[data-cancel-index]'); if (cancelTarget) { handleCancel(cancelTarget.getAttribute('data-cancel-index')); return; } var retryTarget = event.target.closest('[data-retry-tab]'); if (retryTarget) loadTab(retryTarget.getAttribute('data-retry-tab')); }); window.addEventListener('hyapp:cp-space-data', function (event) { applyExternalData(event.detail || {}); }); window.addEventListener('hyapp:i18n-ready', render); } bindEvents(); if (window.HyAppCPSpaceData) applyExternalData(window.HyAppCPSpaceData); else setActiveTab(state.activeTab); if (window.HyAppBridge && window.HyAppBridge.ready) { window.HyAppBridge.ready({ page: 'cp-space' }); } })();