2026-06-18 18:24:24 +08:00

499 lines
18 KiB
JavaScript

(function () {
var DESIGN_WIDTH = 1080;
var VIEWPORT_WIDTH = 375;
var DESIGN_HEIGHT = 4620;
var STORAGE_KEY = 'hy_agency_opening_end_at';
var viewport = document.getElementById('appViewport');
var stageWrap = document.getElementById('stageWrap');
var countdownTimer = 0;
var countdownEndAt = 0;
var timeOffsetMS = 0;
var statusLoadedFromAPI = false;
var state = null;
var countNodes = {
days: document.querySelector('[data-count="days"]'),
hours: document.querySelector('[data-count="hours"]'),
minutes: document.querySelector('[data-count="minutes"]'),
seconds: document.querySelector('[data-count="seconds"]'),
};
var applyButton = document.getElementById('applyButton');
var rewardRows = document.getElementById('rewardRows');
var rulesList = document.getElementById('rulesList');
var fallbackRewards = [
{ opening_revenue: '500K', reward_coins: '50K' },
{ opening_revenue: '1M', reward_coins: '100K' },
{ opening_revenue: '3M', reward_coins: '300K' },
{ opening_revenue: '5M', reward_coins: '500K' },
{ opening_revenue: '10M', reward_coins: '500K' },
];
function params() {
return new URLSearchParams(window.location.search || '');
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function twoDigits(value) {
return String(Math.max(0, value)).padStart(2, '0');
}
function serverNow() {
return Date.now() - timeOffsetMS;
}
function updateViewportHeight() {
var scale = Math.min(window.innerWidth / VIEWPORT_WIDTH, 1);
var visibleHeight = Math.ceil(
DESIGN_HEIGHT * (VIEWPORT_WIDTH / DESIGN_WIDTH)
);
document.documentElement.style.setProperty(
'--app-scale',
String(scale)
);
stageWrap.style.height = visibleHeight + 'px';
viewport.style.minHeight = visibleHeight + 'px';
document.body.style.minHeight = Math.ceil(visibleHeight * scale) + 'px';
}
function fallbackEndAt() {
var queryEnd = Date.parse(
params().get('end_at') || params().get('endAt') || ''
);
if (Number.isFinite(queryEnd) && queryEnd > Date.now()) return queryEnd;
var cached = Number(window.localStorage.getItem(STORAGE_KEY));
if (Number.isFinite(cached) && cached > Date.now()) return cached;
var fallback =
Date.now() +
4 * 86400 * 1000 +
4 * 3600 * 1000 +
4 * 60 * 1000 +
4000;
window.localStorage.setItem(STORAGE_KEY, String(fallback));
return fallback;
}
function updateCountdown() {
var diff = Math.max(0, countdownEndAt - serverNow());
var totalSeconds = Math.floor(diff / 1000);
countNodes.days.textContent = twoDigits(
Math.floor(totalSeconds / 86400)
);
countNodes.hours.textContent = twoDigits(
Math.floor((totalSeconds % 86400) / 3600)
);
countNodes.minutes.textContent = twoDigits(
Math.floor((totalSeconds % 3600) / 60)
);
countNodes.seconds.textContent = twoDigits(totalSeconds % 60);
}
function firstValue(source, keys, fallback) {
if (!source) return fallback;
for (var index = 0; index < keys.length; index += 1) {
var value = source[keys[index]];
if (value !== undefined && value !== null && value !== '')
return value;
}
return fallback;
}
function normalizeStatus(raw) {
var data = raw || {};
var qualification =
data.qualification || data.qualifications || data.agency || {};
var record =
data.record || data.opening_record || data.openingRecord || {};
var application =
data.application || data.opening_application || data.openingApplication || {};
var joined = Boolean(
firstValue(data, ['joined', 'has_joined', 'hasJoined'], false)
);
var eligible = firstValue(
data,
['eligible', 'can_apply', 'canApply'],
true
);
var ineligibleReason = String(
firstValue(
data,
['ineligible_reason', 'ineligibleReason'],
''
) || ''
);
var hostCount =
Number(
firstValue(qualification, ['host_count', 'hostCount'], 12)
) || 0;
var hostRequirement =
Number(
firstValue(
qualification,
['host_requirement', 'hostRequirement'],
10
)
) || 10;
return {
serverTimeMS:
Number(
firstValue(
data,
['server_time_ms', 'serverTimeMs'],
Date.now()
)
) || Date.now(),
endAt:
Number(
firstValue(
data.period || data.activity_period || {},
['end_ms', 'endMs'],
firstValue(data, ['period_end_ms', 'periodEndMs'], 0)
)
) || fallbackEndAt(),
qualification: {
identity: firstValue(
qualification,
['identity', 'agency_identity', 'agencyIdentity'],
'Agency'
),
hostCount: hostCount,
hostRequirement: hostRequirement,
openingStatus: firstValue(
qualification,
[
'opening_status',
'openingStatus',
'status_text',
'statusText',
],
joined
? t('agencyOpening.joined', 'Joined')
: t('agencyOpening.notJoined', 'Not Joined')
),
ineligibleReason: ineligibleReason,
eligible: Boolean(eligible) && !joined,
},
rewards: Array.isArray(
data.rewards || data.reward_tiers || data.rewardTiers
)
? data.rewards || data.reward_tiers || data.rewardTiers
: fallbackRewards,
rules: Array.isArray(data.rules) ? data.rules : null,
record: {
guildName: firstValue(
record,
['guild_name', 'guildName', 'agency_name', 'agencyName'],
'Royal Family'
),
created: firstValue(
record,
['created_at', 'createdAt', 'created'],
'2026-06-01'
),
revenue: firstValue(
record,
['opening_revenue', 'openingRevenue', 'revenue'],
'100K'
),
reward: firstValue(
record,
['reward', 'reward_coins', 'rewardCoins'],
'500K'
),
scoreEndMS:
Number(
firstValue(
record,
['score_end_ms', 'scoreEndMs'],
firstValue(
application,
['score_end_ms', 'scoreEndMs'],
0
)
)
) || 0,
},
application: application,
joined: joined,
};
}
function mockStatus() {
var query = params();
return normalizeStatus({
period: { end_ms: fallbackEndAt() },
qualification: {
agency_identity: query.get('identity') || 'Agency',
host_count: query.get('hosts') || 12,
host_requirement: query.get('host_requirement') || 10,
opening_status: query.get('status') || 'Not Joined',
},
record: {
guild_name: query.get('guild') || 'Royal Family',
created_at: query.get('created') || '2026-06-01',
opening_revenue: query.get('revenue') || '100K',
reward: query.get('reward') || '500K',
},
joined: query.get('joined') === '1',
eligible: query.get('eligible') !== '0',
rewards: fallbackRewards,
server_time_ms: Date.now(),
});
}
function fetchStatus() {
if (params().get('mock') === '1' || !window.HyAppAPI) {
statusLoadedFromAPI = false;
return Promise.resolve(mockStatus());
}
return window.HyAppAPI.get('/api/v1/activities/agency-opening-event')
.then(function (data) {
statusLoadedFromAPI = true;
return normalizeStatus(data);
})
.catch(function (error) {
// 新活动页先独立上线,接口未部署或活动未配置时继续渲染 Figma 默认态,避免线上 WebView 出现空白页。
console.warn('agency opening event status fallback', error);
statusLoadedFromAPI = false;
return mockStatus();
});
}
function renderRewards(rewards) {
rewardRows.innerHTML = (
rewards && rewards.length ? rewards : fallbackRewards
)
.map(function (item) {
var revenue = firstValue(
item,
[
'opening_revenue',
'openingRevenue',
'threshold_coin_spent',
'thresholdCoinSpent',
'revenue',
],
'500K'
);
var reward = firstValue(
item,
['reward_coins', 'rewardCoins', 'reward'],
'50K'
);
return [
'<div class="reward-row">',
'<span>',
escapeHTML(formatCoinLabel(revenue)),
'</span>',
'<span>',
escapeHTML(reward),
'</span>',
'</div>',
].join('');
})
.join('');
}
function renderRules(rules) {
if (!rules || !rules.length) return;
rulesList.innerHTML = rules
.map(function (rule) {
return '<li>' + escapeHTML(rule) + '</li>';
})
.join('');
}
function applyStatus(data) {
state = data || mockStatus();
timeOffsetMS = Date.now() - state.serverTimeMS;
countdownEndAt = state.endAt;
var identityQualified =
String(state.qualification.identity || '')
.toLowerCase()
.indexOf('agency') >= 0 &&
state.qualification.ineligibleReason !==
'active_agency_owner_required';
var hostQualified =
Number(state.qualification.hostCount || 0) >
Number(state.qualification.hostRequirement || 0);
var statusQualified =
!state.joined &&
String(state.qualification.openingStatus || '').toLowerCase() !==
'disabled';
document.getElementById('agencyIdentityText').textContent =
state.qualification.identity;
document.getElementById('hostCountText').textContent =
state.qualification.hostCount +
' > ' +
state.qualification.hostRequirement;
document.getElementById('openingStatusText').textContent =
state.qualification.openingStatus;
setQualified('agencyIdentityCard', identityQualified);
setQualified('hostCountCard', hostQualified);
setQualified('openingStatusCard', statusQualified);
document.getElementById('eligibilityText').textContent = state
.qualification.eligible
? t('agencyOpening.eligible', 'Eligible To Apply')
: t('agencyOpening.notEligible', 'Not Eligible');
document.getElementById('recordGuildName').textContent =
state.record.guildName;
document.getElementById('recordCreated').textContent =
state.record.created;
document.getElementById('recordRevenue').textContent =
state.record.revenue;
document.getElementById('recordReward').textContent =
state.record.reward;
applyButton.classList.toggle('is-joined', state.joined);
applyButton.querySelector('span').textContent = state.joined
? t('agencyOpening.applied', 'Applied')
: t('agencyOpening.apply', 'Apply Opening Event');
renderRewards(state.rewards);
renderRules(state.rules);
updateCountdown();
if (countdownTimer) window.clearInterval(countdownTimer);
countdownTimer = window.setInterval(updateCountdown, 1000);
}
function setQualified(id, qualified) {
var node = document.getElementById(id);
if (!node) return;
node.classList.toggle('is-qualified', Boolean(qualified));
}
function showToast(message) {
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
return;
}
window.dispatchEvent(
new CustomEvent('hyapp:toast', { detail: { message: message } })
);
}
function commandID() {
var random = Math.random().toString(36).slice(2);
if (window.crypto && window.crypto.getRandomValues) {
var bytes = new Uint32Array(2);
window.crypto.getRandomValues(bytes);
random = bytes[0].toString(36) + bytes[1].toString(36);
}
return 'agency_opening_' + Date.now().toString(36) + '_' + random;
}
function submitApply() {
if (
!state ||
state.joined ||
applyButton.classList.contains('is-loading')
)
return;
if (!state.qualification.eligible) {
showToast(t('agencyOpening.notEligible', 'Not Eligible'));
return;
}
applyButton.classList.add('is-loading');
// 申请动作按 command_id 做点击级幂等;如果后端接口还没上线,页面只更新本地展示态,不伪造服务端成功事实。
var request = statusLoadedFromAPI
? window.HyAppAPI.post(
'/api/v1/activities/agency-opening-event/apply',
{
command_id: commandID(),
}
)
: Promise.resolve({
joined: true,
qualification: {
opening_status: t(
'agencyOpening.pendingReview',
'Pending Review'
),
},
});
request
.then(function (data) {
var next = Object.assign({}, state);
var payload = data || {};
var payloadQualification =
payload.qualification || payload.qualifications || {};
next.joined = true;
next.qualification = Object.assign({}, state.qualification, {
openingStatus: firstValue(
payloadQualification,
[
'opening_status',
'openingStatus',
'status_text',
'statusText',
],
t('agencyOpening.pendingReview', 'Pending Review')
),
});
next.qualification.eligible = false;
applyStatus(next);
showToast(
t('agencyOpening.applySuccess', 'Application submitted.')
);
})
.catch(function (error) {
console.warn('agency opening event apply failed', error);
showToast(
t(
'agencyOpening.applyFailed',
'Application failed. Try again later.'
)
);
})
.finally(function () {
applyButton.classList.remove('is-loading');
});
}
function escapeHTML(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function formatCoinLabel(value) {
var amount = Number(value);
if (!Number.isFinite(amount) || amount <= 0) return value;
if (amount % 1000000 === 0) return amount / 1000000 + 'M';
if (amount % 1000 === 0) return amount / 1000 + 'K';
return String(amount);
}
function init() {
document.title = t(
'agencyOpening.pageTitle',
'New Agency Opening Event'
);
updateViewportHeight();
fetchStatus().then(applyStatus);
applyButton.addEventListener('click', submitApply);
window.addEventListener('resize', updateViewportHeight);
window.addEventListener('hyapp:i18n-ready', function () {
document.title = t(
'agencyOpening.pageTitle',
'New Agency Opening Event'
);
if (state) applyStatus(state);
});
}
init();
})();