Files

648 lines
26 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 () {
const body = document.body;
const subtitleEl = document.getElementById('season-subtitle');
const bannerEl = document.getElementById('season-banner');
const profileEl = document.getElementById('season-profile');
const announcementsEl = document.getElementById('season-announcements');
const cardsEl = document.getElementById('season-cards');
const tasksEl = document.getElementById('season-tasks');
const rewardsEl = document.getElementById('season-rewards');
const decorationsEl = document.getElementById('season-decorations');
const heroCoverEl = document.getElementById('season-hero-cover');
const heroTitleEl = document.getElementById('season-hero-title');
const heroSubEl = document.getElementById('season-hero-sub');
const dailyCardEl = document.getElementById('season-daily-card');
const dailyStoryEl = document.getElementById('season-daily-story');
const dailyStoryHintEl = document.getElementById('season-daily-story-hint');
const intelEl = document.getElementById('season-intel');
const intelUnlockBtn = document.getElementById('season-intel-unlock-btn');
const statusEl = document.getElementById('status');
const cardRefreshHintEl = document.getElementById('season-card-refresh-hint');
const passUnlockBtn = document.getElementById('season-pass-unlock-btn');
const claimAllTasksBtn = document.getElementById('season-claim-all-tasks-btn');
const chroniclePinEl = document.getElementById('season-chronicle-pin');
const seasonSwitchEl = document.getElementById('season-switch');
const seasonSwitchCardsEl = document.getElementById('season-switch-cards');
const seasonSwitchChronicleEl = document.getElementById('season-switch-chronicle');
const tabAllBtn = document.getElementById('season-task-tab-all');
const tabDailyBtn = document.getElementById('season-task-tab-daily');
const tabWeeklyBtn = document.getElementById('season-task-tab-weekly');
const tabSeasonBtn = document.getElementById('season-task-tab-season');
let currentTaskFilter = '';
let cachedPayload = null;
const setStatus = (text) => {
if (statusEl) statusEl.textContent = text || '';
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const postJson = async (url, payload = {}) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const getCardDetail = async (cardId) => {
const safeId = String(cardId || '').trim();
if (!safeId) return null;
const res = await fetch(`/api/cards/${encodeURIComponent(safeId)}`);
const data = await res.json();
if (!data.ok) return null;
return data.card || null;
};
const levelBadge = (rarity) => {
const key = String(rarity || '').toLowerCase();
if (key === 'legendary') return '传说';
if (key === 'epic') return '史诗';
if (key === 'rare') return '稀有';
return '普通';
};
const formatCountdown = (nextRefreshAt) => {
const text = String(nextRefreshAt || '').trim();
if (!text) return '下次刷新时间:-';
const nextTs = Date.parse(text);
if (!Number.isFinite(nextTs)) return `下次刷新时间:${text}`;
const diff = Math.max(0, nextTs - Date.now());
const hour = Math.floor(diff / (1000 * 60 * 60));
const minute = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
return `距下次刷新约 ${hour} 小时 ${minute} 分钟(${text})`;
};
const renderBanner = (payload) => {
if (!bannerEl) return;
const season = payload.season || {};
const profile = payload.profile || {};
bannerEl.textContent = '';
if (heroCoverEl) {
const cover = String(season.cover_url || '').trim();
if (cover) heroCoverEl.style.setProperty('--season-cover', `url('${cover.replace(/'/g, "\\'")}')`);
}
const rows = [
`赛季:${season.name || '-'}`,
`主题:${season.theme || '-'}`,
`状态:${season.time_status || '-'}`,
`时间:${season.start_at || '-'} ~ ${season.end_at || '-'}`,
season.rest_end_at ? `休整期截止:${season.rest_end_at}` : '',
`通行证价格:${Number(season.pass_price || 0)} 积分`,
`赛季等级:Lv.${Number(profile.level || 1)}(总经验 ${Number(profile.xp || 0)})`,
`赛季币:${Number(profile.season_points || 0)}`,
].filter(Boolean);
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
bannerEl.appendChild(div);
});
const titleText = `${season.name || '赛季'} · ${season.theme || '未命名主题'}`;
if (subtitleEl) subtitleEl.textContent = titleText;
if (heroTitleEl) heroTitleEl.textContent = season.name || '赛季中心';
if (heroSubEl) heroSubEl.textContent = titleText;
if (cardRefreshHintEl) {
cardRefreshHintEl.textContent = formatCountdown(payload.next_refresh_at || '');
}
};
const renderSeasonCardCenter = (payload) => {
const season = payload.season || {};
const seasonId = String(season.season_id || '').trim();
if (!seasonId) return;
const parent = dailyCardEl ? dailyCardEl.parentElement : null;
if (!parent) return;
let box = document.getElementById('season-card-center');
if (!box) {
box = document.createElement('div');
box.id = 'season-card-center';
box.className = 'admin-inline-actions';
box.style.marginTop = '0.4rem';
parent.insertBefore(box, dailyCardEl);
}
box.textContent = '';
const link = document.createElement('a');
link.className = 'link-btn';
link.href = `/season/${encodeURIComponent(seasonId)}/cards`;
link.textContent = '进入本赛季游戏卡列表';
box.appendChild(link);
};
const renderProfile = (payload) => {
if (!profileEl) return;
const profile = payload.profile || {};
profileEl.textContent = '';
const rows = [
`通行证:${profile.pass_type === 'premium' ? '高级档' : '免费档'}`,
`当前等级:${Number(profile.level || 1)}`,
`本级经验:${Number(profile.xp_in_current_level || 0)} / ${Number(profile.xp_per_level || 100)}`,
`距离下一级:${Number(profile.xp_to_next_level || 0)}`,
`已完成任务:${Number(profile.completed_tasks || 0)}`,
`已领取卡片:${Number(profile.total_claimed_cards || 0)}`,
];
rows.forEach((text) => {
const div = document.createElement('div');
div.textContent = text;
profileEl.appendChild(div);
});
if (passUnlockBtn) {
passUnlockBtn.disabled = profile.pass_type === 'premium';
passUnlockBtn.textContent = profile.pass_type === 'premium' ? '已解锁高级通行证' : '解锁高级通行证';
}
};
const renderAnnouncements = (rows) => {
if (!announcementsEl) return;
announcementsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无赛季公告';
announcementsEl.appendChild(li);
return;
}
list.forEach((row) => {
const li = document.createElement('li');
li.textContent = `${row.title || '公告'} · ${row.created_at || '-'} · ${row.content || ''}`;
announcementsEl.appendChild(li);
});
};
const buildActionBtn = (text, onClick, className = 'secondary-btn') => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = className;
btn.textContent = text;
btn.addEventListener('click', onClick);
return btn;
};
const renderCards = (rows, payload) => {
if (!cardsEl) return;
cardsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '当前刷新槽位暂无卡片,稍后重试。';
cardsEl.appendChild(li);
return;
}
list.forEach((row) => {
const li = document.createElement('li');
const head = document.createElement('div');
head.textContent = `【${levelBadge(row.rarity)}】${row.title || '未命名卡片'} · ${row.status || 'active'}`;
li.appendChild(head);
const story = document.createElement('p');
story.className = 'helper-text';
story.textContent = row.story_text || '';
li.appendChild(story);
const meta = document.createElement('p');
meta.className = 'helper-text';
meta.textContent = `目标:${row.objective || '-'} | 进度 ${Number(row.progress_value || 0)}/${Number(row.target_value || 1)} | 奖励 ${Number(row.reward_points || 0)} 积分 + ${Number(row.reward_xp || 0)} 经验`;
li.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'admin-inline-actions';
if (String(row.status || '') === 'completed') {
actions.appendChild(buildActionBtn('领取奖励', async () => {
setStatus(`正在领取卡片奖励 #${row.id}...`);
const data = await postJson(`/api/seasons/active/cards/${row.id}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('卡片奖励领取成功。');
await reload();
}));
}
if (String(row.status || '') === 'active') {
actions.appendChild(buildActionBtn('手动刷新', async () => {
setStatus(`正在刷新卡片 #${row.id}...`);
const data = await postJson(`/api/seasons/active/cards/${row.id}/refresh`, {});
if (!data.ok) {
setStatus(data.error || '刷新失败');
return;
}
setStatus('卡片已刷新。');
await reload();
}, 'ghost-btn'));
}
if (actions.children.length) li.appendChild(actions);
cardsEl.appendChild(li);
});
if (cardRefreshHintEl) {
cardRefreshHintEl.textContent = formatCountdown(payload.next_refresh_at || '');
}
};
const renderTasks = (rows) => {
if (!tasksEl) return;
tasksEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
const filtered = currentTaskFilter
? list.filter((row) => String(row.task_type || '') === currentTaskFilter)
: list;
if (!filtered.length) {
const li = document.createElement('li');
li.textContent = '暂无任务';
tasksEl.appendChild(li);
return;
}
filtered.forEach((row) => {
const li = document.createElement('li');
const title = document.createElement('div');
title.textContent = `[${row.task_type || '-'}] ${row.title || '未命名任务'} · ${row.status || 'active'}`;
li.appendChild(title);
const desc = document.createElement('p');
desc.className = 'helper-text';
desc.textContent = `${row.description || ''} | 进度 ${Number(row.progress_value || 0)}/${Number(row.target_value || 1)} | 奖励 ${Number(row.reward_points || 0)} 积分 + ${Number(row.reward_xp || 0)} 经验`;
li.appendChild(desc);
if (String(row.status || '') === 'completed') {
li.appendChild(buildActionBtn('领取任务奖励', async () => {
setStatus(`正在领取任务 #${row.id}...`);
const data = await postJson(`/api/seasons/active/tasks/${row.id}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('任务奖励领取成功。');
await reload();
}));
}
tasksEl.appendChild(li);
});
};
const renderRewards = (rows, claimedIds, profile) => {
if (!rewardsEl) return;
rewardsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
const claimedSet = new Set(Array.isArray(claimedIds) ? claimedIds : []);
const level = Number(profile?.level || 1);
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂无通行证奖励配置';
rewardsEl.appendChild(li);
return;
}
list.slice(0, 120).forEach((row) => {
const rewardId = String(row.reward_id || '');
const li = document.createElement('li');
const locked = level < Number(row.reward_level || 1);
const claimed = claimedSet.has(rewardId);
const title = `${row.title || rewardId} · Lv.${Number(row.reward_level || 1)} · ${row.reward_track || 'free'}`;
li.textContent = `${title} | 奖励 ${Number(row.reward_points || 0)} 积分`;
if (!locked && !claimed) {
li.appendChild(buildActionBtn('领取', async () => {
setStatus(`正在领取赛季奖励 ${rewardId}...`);
const data = await postJson(`/api/seasons/active/rewards/${encodeURIComponent(rewardId)}/claim`, {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus('赛季奖励领取成功。');
await reload();
}));
} else {
const mark = document.createElement('span');
mark.className = 'helper-text';
mark.style.marginLeft = '0.5rem';
mark.textContent = claimed ? '已领取' : '未解锁';
li.appendChild(mark);
}
rewardsEl.appendChild(li);
});
};
const renderDecorations = (rows, equipped) => {
if (!decorationsEl) return;
decorationsEl.textContent = '';
const list = Array.isArray(rows) ? rows : [];
if (!list.length) {
const li = document.createElement('li');
li.textContent = '暂未获得赛季装扮';
decorationsEl.appendChild(li);
return;
}
const equippedTitleId = String((equipped?.title || {}).decoration_id || '');
const equippedFrameId = String((equipped?.avatar_frame || {}).decoration_id || '');
list.forEach((row) => {
const type = String(row.decoration_type || '');
const decorationId = String(row.decoration_id || '');
const isEquipped = (type === 'title' && decorationId === equippedTitleId)
|| (type === 'avatar_frame' && decorationId === equippedFrameId)
|| Boolean(row.is_equipped);
const li = document.createElement('li');
li.textContent = `${type === 'title' ? '称号' : '头像框'}:${decorationId}${isEquipped ? '(已佩戴)' : ''}`;
if (!isEquipped) {
li.appendChild(buildActionBtn('佩戴', async () => {
setStatus(`正在佩戴 ${decorationId}...`);
const data = await postJson('/api/seasons/decorations/equip', {
decoration_type: type,
decoration_id: decorationId,
});
if (!data.ok) {
setStatus(data.error || '佩戴失败');
return;
}
setStatus('佩戴成功。');
await reload();
}, 'ghost-btn'));
}
decorationsEl.appendChild(li);
});
};
const renderDailyCard = (payload) => {
if (!dailyCardEl) return;
dailyCardEl.textContent = '';
const daily = payload.daily_game_card || {};
const title = String(daily.title || '').trim() || '今日赛季日卡未生成';
const summary = String(daily.summary || '').trim();
const cardId = String(daily.card_id || '').trim();
const head = document.createElement('div');
head.className = 'season-daily-head';
head.textContent = title;
dailyCardEl.appendChild(head);
if (summary) {
const p = document.createElement('p');
p.className = 'helper-text';
p.textContent = summary;
dailyCardEl.appendChild(p);
}
if (cardId) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'secondary-btn';
btn.textContent = '用今日游戏卡开一局';
btn.addEventListener('click', async () => {
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: cardId });
if (!data.ok) {
setStatus(data.error || '创建失败');
return;
}
if (data.player_id) localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
if (data.player_name) localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
});
dailyCardEl.appendChild(btn);
}
const hint = document.createElement('p');
hint.className = 'helper-text';
hint.textContent = '提示:通关这张日卡后,才能解锁当日资料。';
dailyCardEl.appendChild(hint);
};
const renderDailyStory = async (payload) => {
if (!dailyStoryEl) return;
dailyStoryEl.textContent = '';
const daily = payload.daily_game_card || {};
const cardId = String(daily.card_id || '').trim();
if (!cardId) {
dailyStoryEl.textContent = '今日故事未生成';
return;
}
const card = await getCardDetail(cardId);
if (!card) {
dailyStoryEl.textContent = '今日故事加载失败';
return;
}
const title = document.createElement('div');
title.className = 'season-daily-head';
title.textContent = `“${card.title || '未命名故事'}”`;
dailyStoryEl.appendChild(title);
const summary = document.createElement('p');
summary.className = 'helper-text';
summary.textContent = card.summary || '';
dailyStoryEl.appendChild(summary);
const goals = Array.isArray(card.starter_goals) ? card.starter_goals : [];
const objective = document.createElement('p');
objective.className = 'helper-text';
objective.textContent = goals[0] || '探索今日线索并推进剧情';
dailyStoryEl.appendChild(objective);
const reward = document.createElement('div');
reward.className = 'helper-text';
reward.textContent = '奖励:赛季经验/积分结算';
dailyStoryEl.appendChild(reward);
const actions = document.createElement('div');
actions.className = 'admin-inline-actions';
const acceptBtn = document.createElement('button');
acceptBtn.type = 'button';
acceptBtn.className = 'secondary-btn';
acceptBtn.textContent = '接受挑战';
acceptBtn.addEventListener('click', async () => {
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: cardId });
if (!data.ok) {
setStatus(data.error || '创建失败');
return;
}
if (data.player_id) localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
if (data.player_name) localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
});
const refreshBtn = document.createElement('button');
refreshBtn.type = 'button';
refreshBtn.className = 'ghost-btn';
refreshBtn.textContent = '刷新';
refreshBtn.addEventListener('click', async () => {
setStatus('正在刷新今日故事...');
await reload();
setStatus('今日故事已刷新。');
});
actions.appendChild(acceptBtn);
actions.appendChild(refreshBtn);
dailyStoryEl.appendChild(actions);
if (dailyStoryHintEl) {
const locked = Array.isArray(daily.locked_intel) ? daily.locked_intel.length : 0;
const unlocked = Array.isArray(payload.intel)
? payload.intel.filter((it) => String(it.day_key || '') === String(daily.day_key || '')).length
: 0;
const pending = Math.max(0, locked - unlocked);
dailyStoryHintEl.textContent = pending ? `还有 ${pending} 条线索待查看 →` : '今日线索已全部查看';
}
};
const renderIntel = (payload) => {
if (!intelEl) return;
intelEl.textContent = '';
const rows = Array.isArray(payload.intel) ? payload.intel : [];
if (!rows.length) {
const li = document.createElement('li');
li.textContent = '暂无资料,先解锁一条。';
intelEl.appendChild(li);
return;
}
rows.slice(0, 80).forEach((row) => {
const li = document.createElement('li');
li.textContent = `${row.intel_title || '资料'} · ${row.day_key || '-'} · ${row.intel_text || ''}`;
intelEl.appendChild(li);
});
};
const renderTaskTabs = () => {
const update = () => {
[tabAllBtn, tabDailyBtn, tabWeeklyBtn, tabSeasonBtn].forEach((btn) => {
if (!btn) return;
btn.classList.remove('secondary-btn');
});
if (!currentTaskFilter && tabAllBtn) tabAllBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'daily' && tabDailyBtn) tabDailyBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'weekly' && tabWeeklyBtn) tabWeeklyBtn.classList.add('secondary-btn');
if (currentTaskFilter === 'season' && tabSeasonBtn) tabSeasonBtn.classList.add('secondary-btn');
if (cachedPayload) renderTasks(cachedPayload.tasks || []);
};
if (tabAllBtn) tabAllBtn.addEventListener('click', () => { currentTaskFilter = ''; update(); });
if (tabDailyBtn) tabDailyBtn.addEventListener('click', () => { currentTaskFilter = 'daily'; update(); });
if (tabWeeklyBtn) tabWeeklyBtn.addEventListener('click', () => { currentTaskFilter = 'weekly'; update(); });
if (tabSeasonBtn) tabSeasonBtn.addEventListener('click', () => { currentTaskFilter = 'season'; update(); });
update();
};
const bindTopActions = () => {
if (passUnlockBtn) {
passUnlockBtn.addEventListener('click', async () => {
setStatus('正在解锁高级通行证...');
const data = await postJson('/api/seasons/active/pass/unlock', {});
if (!data.ok) {
setStatus(data.error || '解锁失败');
return;
}
setStatus('高级通行证已解锁。');
await reload();
});
}
if (claimAllTasksBtn) {
claimAllTasksBtn.addEventListener('click', async () => {
setStatus('正在一键领取任务奖励...');
const data = await postJson('/api/seasons/active/tasks/claim-all', {});
if (!data.ok) {
setStatus(data.error || '领取失败');
return;
}
setStatus(`已领取 ${Number((data.result || {}).claimed_count || 0)} 个任务奖励。`);
await reload();
});
}
};
const bindIntelActions = () => {
if (!intelUnlockBtn) return;
intelUnlockBtn.addEventListener('click', async () => {
const payload = cachedPayload || {};
const daily = payload.daily_game_card || {};
const dayKey = String(daily.day_key || '').trim();
if (!dayKey) {
setStatus('今日赛季日卡尚未生成,稍后再试。');
return;
}
setStatus('正在解锁资料...');
const data = await postJson('/api/seasons/active/intel/unlock', { day_key: dayKey });
if (!data.ok) {
if (String(data.error || '').includes('通关')) {
setStatus(data.error);
} else {
setStatus(data.error || '解锁失败');
}
return;
}
setStatus(`已解锁资料:${(data.unlocked || {}).intel_title || ''}`);
await reload();
});
};
const renderAll = (payload) => {
cachedPayload = payload || {};
renderBanner(payload || {});
renderProfile(payload || {});
renderAnnouncements(payload.announcements || []);
renderSeasonCardCenter(payload || {});
bindSeasonSwitch(payload || {});
renderDailyStory(payload || {}).catch((error) => setStatus(`网络错误: ${error}`));
renderDailyCard(payload || {});
renderIntel(payload || {});
renderCards(payload.cards || [], payload || {});
renderTasks(payload.tasks || []);
renderRewards(payload.rewards || [], payload.claimed_reward_ids || [], payload.profile || {});
renderDecorations(payload.decorations || [], payload.equipped_decorations || {});
const timeStatus = String((payload.season || {}).time_status || '');
if (chroniclePinEl) {
chroniclePinEl.style.display = timeStatus === 'rest' ? '' : 'none';
}
};
const bindSeasonSwitch = (payload) => {
if (!seasonSwitchEl || !seasonSwitchCardsEl || !seasonSwitchChronicleEl) return;
if (seasonSwitchEl.dataset.bound === '1') return;
seasonSwitchEl.dataset.bound = '1';
const applyLinks = () => {
const seasonId = String(seasonSwitchEl.value || '').trim();
seasonSwitchCardsEl.href = seasonId ? `/season/${encodeURIComponent(seasonId)}/cards` : '#';
seasonSwitchChronicleEl.href = seasonId ? `/season/${encodeURIComponent(seasonId)}/chronicle` : '#';
seasonSwitchCardsEl.classList.toggle('disabled', !seasonId);
seasonSwitchChronicleEl.classList.toggle('disabled', !seasonId);
};
seasonSwitchEl.addEventListener('change', applyLinks);
applyLinks();
(async () => {
const res = await getJson('/api/seasons/history');
if (!res.ok) {
setStatus(res.error || '加载赛季列表失败');
return;
}
const seasons = Array.isArray(res.seasons) ? res.seasons : [];
const currentId = String((payload.season || {}).season_id || '').trim();
seasonSwitchEl.textContent = '';
seasons.slice(0, 60).forEach((s) => {
const sid = String((s || {}).season_id || '').trim();
if (!sid) return;
const opt = document.createElement('option');
const name = String((s || {}).name || '').trim() || '未命名赛季';
const theme = String((s || {}).theme || '').trim();
const status = String((s || {}).time_status || '').trim();
opt.value = sid;
opt.textContent = `${name}${theme ? ` · ${theme}` : ''}${status ? ` · ${status}` : ''}`;
if (sid === currentId) opt.selected = true;
seasonSwitchEl.appendChild(opt);
});
applyLinks();
})().catch((error) => setStatus(`网络错误: ${error}`));
};
const reload = async () => {
const data = await getJson('/api/seasons/active');
if (!data.ok) {
setStatus(data.error || '加载赛季信息失败');
return;
}
renderAll(data);
};
bindTopActions();
bindIntelActions();
renderTaskTabs();
reload().catch((error) => setStatus(`网络错误: ${error}`));
})();