499 lines
18 KiB
JavaScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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();
|
|
})();
|