2026-07-09 13:25:54 +08:00

369 lines
13 KiB
JavaScript
Raw Permalink 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 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' });
}
})();