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