feat(pages): implement web (raw) data visualization tab

This commit is contained in:
daydreamer-json
2026-03-08 03:37:48 +09:00
parent 5e2c00dcb8
commit 9f59093577
2 changed files with 161 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import { renderLaunchers } from './renderers/launchers.js';
import { renderOverview } from './renderers/overview.js';
import { renderPatches } from './renderers/patches.js';
import { renderResources } from './renderers/resources.js';
import { renderWeb } from './renderers/web.js';
import type { MirrorFileEntry } from './types.js';
import { BASE_URL } from './utils/constants.js';
@@ -32,6 +33,7 @@ async function main() {
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-patch" type="button">Patches</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-resources" type="button">Resources</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-launcher" type="button">Launcher</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-web" type="button">Web (Raw)</button></li>
</ul>
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="mainTabsContent">
<div class="tab-pane fade show active" id="tab-overview" role="tabpanel"></div>
@@ -39,6 +41,7 @@ async function main() {
<div class="tab-pane fade" id="tab-patch" role="tabpanel"></div>
<div class="tab-pane fade" id="tab-resources" role="tabpanel"></div>
<div class="tab-pane fade" id="tab-launcher" role="tabpanel"></div>
<div class="tab-pane fade" id="tab-web" role="tabpanel"></div>
</div>
`;
contentDiv.innerHTML = tabsHtml;
@@ -49,5 +52,6 @@ async function main() {
renderPatches(document.getElementById('tab-patch')!, mirrorFileDb),
renderResources(document.getElementById('tab-resources')!),
renderLaunchers(document.getElementById('tab-launcher')!, mirrorFileDb),
renderWeb(document.getElementById('tab-web')!),
]);
}

View File

@@ -0,0 +1,157 @@
import { DateTime } from 'luxon';
import { fetchJson } from '../api.js';
import type { StoredData } from '../types.js';
import { BASE_URL, gameTargets, launcherWebApiLang } from '../utils/constants.js';
const apiTypes = ['announcement', 'banner', 'main_bg_image', 'sidebar', 'single_ent'];
export async function renderWeb(container: HTMLElement) {
for (const target of gameTargets) {
const section = document.createElement('div');
section.className = 'mb-5';
section.innerHTML = `<h3 class="mb-3">${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}</h3>`;
const langs = launcherWebApiLang[target.region] || [];
const defaultLang = target.region === 'os' ? 'en-us' : 'zh-cn';
// Language Selector
const langSelectGroup = document.createElement('div');
langSelectGroup.className = 'input-group mb-3';
langSelectGroup.innerHTML = '<span class="input-group-text">Language</span>';
const langSelect = document.createElement('select');
langSelect.className = 'form-select';
langs.forEach((lang) => {
const option = document.createElement('option');
option.value = lang;
option.textContent = lang;
if (lang === defaultLang) {
option.selected = true;
}
langSelect.appendChild(option);
});
langSelectGroup.appendChild(langSelect);
if (langs.length <= 1) {
langSelectGroup.style.display = 'none';
}
section.appendChild(langSelectGroup);
const accordion = document.createElement('div');
accordion.className = 'accordion';
accordion.id = `accordion-web-${target.dirName}`;
const renderApiList = async (lang: string) => {
accordion.innerHTML = '<div class="text-muted p-2">Loading...</div>';
const results = await Promise.all(
apiTypes.map(async (apiType) => {
const url = `${BASE_URL}/akEndfield/launcher/web/${target.dirName}/${apiType}/${lang}/all.json`;
try {
const data = await fetchJson<StoredData<any>[]>(url);
if (!data || data.length === 0) return null;
return { apiType, list: [...data].reverse() };
} catch (e) {
console.warn(`Failed to load ${url}`, e);
return null;
}
}),
);
accordion.innerHTML = '';
const validResults = results.filter((r): r is NonNullable<typeof r> => r !== null);
if (validResults.length === 0) {
accordion.innerHTML = '<div class="text-muted p-2">No data found.</div>';
return;
}
validResults.forEach(({ apiType, list }, idx) => {
const itemId = `web-${target.dirName}-${lang}-${apiType}`;
const item = document.createElement('div');
item.className = 'accordion-item';
// Header
const header = document.createElement('h2');
header.className = 'accordion-header';
header.id = `heading-${itemId}`;
header.innerHTML = `
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${itemId}" aria-expanded="false" aria-controls="collapse-${itemId}">
${apiType}
</button>
`;
item.appendChild(header);
// Body
const collapse = document.createElement('div');
collapse.id = `collapse-${itemId}`;
collapse.className = 'accordion-collapse collapse';
collapse.setAttribute('aria-labelledby', `heading-${itemId}`);
collapse.setAttribute('data-bs-parent', `#${accordion.id}`);
const body = document.createElement('div');
body.className = 'accordion-body';
// Select for UpdatedAt
const selectGroup = document.createElement('div');
selectGroup.className = 'input-group mb-3';
selectGroup.innerHTML = `<span class="input-group-text">History</span>`;
const select = document.createElement('select');
select.className = 'form-select';
select.ariaLabel = 'Select version';
list.forEach((entry, idx) => {
const dateStr = DateTime.fromISO(entry.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss');
const option = document.createElement('option');
option.value = idx.toString();
option.textContent = `${dateStr}`;
select.appendChild(option);
});
selectGroup.appendChild(select);
body.appendChild(selectGroup);
// Content Area
const contentArea = document.createElement('pre');
contentArea.className = 'p-3 border rounded overflow-auto';
contentArea.style.maxHeight = '500px';
contentArea.style.fontSize = '0.875rem';
const updateContent = (index: number) => {
const entry = list[index];
if (entry) {
contentArea.textContent = JSON.stringify(entry.rsp, null, 2);
}
};
// Initial render for this item
updateContent(0);
select.addEventListener('change', (e) => {
const val = parseInt((e.target as HTMLSelectElement).value, 10);
updateContent(val);
});
body.appendChild(contentArea);
collapse.appendChild(body);
item.appendChild(collapse);
accordion.appendChild(item);
});
};
langSelect.addEventListener('change', (e) => {
renderApiList((e.target as HTMLSelectElement).value);
});
section.appendChild(accordion);
container.appendChild(section);
// Initial load
if (defaultLang) {
renderApiList(defaultLang);
}
}
}