259 lines
11 KiB
JavaScript
259 lines
11 KiB
JavaScript
// 宇森官网 · 公开页数据打通(图鉴/规则/论坛/公告 从 Odoo 取数)
|
|
// 依赖 assets/game-api.js
|
|
|
|
// ---------- 页面板块:特色 / 背景 / 致玩家 合并进「背景」单页(平铺,非子菜单) ----------
|
|
function featuresHTML(p){
|
|
const items = (p.items || []).map(it =>
|
|
`<div class="card"><div class="ic">${it.icon || ''}</div><h3>${it.title || ''}</h3><p>${it.desc || ''}</p></div>`
|
|
).join('');
|
|
return `<div class="sec-title">${p.title || '游戏特色'}<small>${p.subtitle || ''}</small></div><div class="grid">${items}</div>`;
|
|
}
|
|
|
|
function backgroundHTML(p){
|
|
const layers = (p.items || []).map((it, idx) =>
|
|
`<div class="layer l${idx + 1}"><div class="lt">${it.title || ''}</div><div class="ld">${it.desc || ''}</div><span class="star">${it.icon || '✦'}</span></div>`
|
|
).join('');
|
|
return `<div class="layers">${layers}</div><div class="bg-text"><h2>${p.title || '世界起源'}</h2>${p.body_html || ''}</div>`;
|
|
}
|
|
|
|
function letterHTML(p){
|
|
return `<div class="letter-wrap"><h2>${p.title || ''}</h2><div class="lt-sub">${p.subtitle || ''}</div><div class="lt-body">${p.body_html || ''}</div><div class="lt-sign">—— 宇森开发组</div></div>`;
|
|
}
|
|
|
|
async function loadPages(){
|
|
try {
|
|
const d = await API.pages();
|
|
const pages = (d && d.items) || [];
|
|
const bg = document.getElementById('background');
|
|
let html = '';
|
|
pages.forEach(p => {
|
|
if (p.key === 'features') html += featuresHTML(p);
|
|
else if (p.key === 'background') html += backgroundHTML(p);
|
|
else if (p.key === 'letter') html += letterHTML(p);
|
|
});
|
|
if (bg) bg.innerHTML = html; // 特色 + 背景 + 致玩家 平铺进同一页,无子菜单切换
|
|
} catch (e) { /* 失败时保留 HTML 中的静态兜底内容 */ }
|
|
}
|
|
|
|
// ---------- 规则 ----------
|
|
async function loadRules(){
|
|
const el = document.querySelector('#rules .rules-scroll');
|
|
if (!el) return;
|
|
try {
|
|
const d = await API.rules();
|
|
const groups = (d && d.groups) || [];
|
|
if (!groups.length){ return; }
|
|
el.innerHTML = groups.map(g => {
|
|
const rows = (g.items || []).map(it => {
|
|
const lvl = (it.level != null) ? ('Lv.' + it.level) : (g.key === 'world' ? '阶段' : '');
|
|
return `<div class="tier-row"><span class="tn">${lvl}</span><span class="tname">${it.name}</span><span class="tdesc">${it.desc || ''}</span></div>`;
|
|
}).join('');
|
|
return `<div class="rule-block"><h3>${g.label}</h3><div class="tier-list">${rows}</div></div>`;
|
|
}).join('');
|
|
} catch (e) { /* 保留静态兜底内容 */ }
|
|
}
|
|
|
|
// ---------- 图鉴 ----------
|
|
async function loadCodex(){
|
|
const grid = document.getElementById('codexGrid');
|
|
const catsEl = document.getElementById('codexCats');
|
|
const subsEl = document.getElementById('codexSubs');
|
|
const searchEl= document.getElementById('codexSearch');
|
|
const countEl = document.getElementById('codexCount');
|
|
if (!grid) return;
|
|
|
|
let data;
|
|
try { data = await API.codex(); }
|
|
catch (e) { return; } // 失败时保留静态兜底内容
|
|
|
|
const groups = (data && data.groups) || [];
|
|
if (!groups.length){
|
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">暂无图鉴数据</div>';
|
|
return;
|
|
}
|
|
|
|
// 选中状态
|
|
let curGroup = groups[0].key;
|
|
let curSub = 'all';
|
|
let curQ = '';
|
|
|
|
function renderCats(){
|
|
if (!catsEl) return;
|
|
catsEl.innerHTML = groups.map(g =>
|
|
`<button class="${g.key === curGroup ? 'active' : ''}" data-gk="${g.key}">${g.label}</button>`
|
|
).join('');
|
|
catsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
|
|
curGroup = b.dataset.gk; curSub = 'all'; curQ = '';
|
|
if (searchEl) searchEl.value = '';
|
|
renderCats(); renderSubs(); renderGrid();
|
|
}));
|
|
}
|
|
|
|
function renderSubs(){
|
|
if (!subsEl) return;
|
|
const g = groups.find(x => x.key === curGroup);
|
|
const subs = (g && g.subs) || [];
|
|
subsEl.innerHTML = `<button class="${curSub === 'all' ? 'active' : ''}" data-sn="all">全部</button>` +
|
|
subs.map(s => `<button class="${curSub === s.name ? 'active' : ''}" data-sn="${s.name}">${s.name}</button>`).join('');
|
|
subsEl.querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
|
|
curSub = b.dataset.sn; renderSubs(); renderGrid();
|
|
}));
|
|
}
|
|
|
|
function renderGrid(){
|
|
const g = groups.find(x => x.key === curGroup);
|
|
if (!g){ grid.innerHTML = ''; return; }
|
|
// 按子分类筛选
|
|
let items = [];
|
|
(g.subs || []).forEach(s => {
|
|
(s.items || []).forEach(it => {
|
|
if (curSub !== 'all' && it.sub !== curSub) return;
|
|
items.push(it);
|
|
});
|
|
});
|
|
// 搜索过滤
|
|
if (curQ){
|
|
const q = curQ.toLowerCase();
|
|
items = items.filter(it =>
|
|
(it.name || '').toLowerCase().includes(q) ||
|
|
(it.desc || '').replace(/<[^>]+>/g, '').toLowerCase().includes(q)
|
|
);
|
|
}
|
|
if (countEl) countEl.textContent = items.length + ' 项';
|
|
if (!items.length){
|
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--text-dim);padding:60px 0">未找到匹配项</div>';
|
|
return;
|
|
}
|
|
const base = API.base || '';
|
|
grid.innerHTML = items.map(it => {
|
|
const thumb = it.image
|
|
? `<div class="thumb" style="background:url('${base}${it.image}') center/cover"></div>`
|
|
: `<div class="thumb"></div>`;
|
|
return `<div class="cx">
|
|
${thumb}
|
|
<div class="body">
|
|
<div class="nm">${it.name || ''}</div>
|
|
<div class="desc">${it.desc || ''}</div>
|
|
<div class="meta"><span class="sub">${it.sub || ''}</span><span class="tier">${it.tier || ''}</span></div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
if (searchEl){
|
|
searchEl.addEventListener('input', () => { curQ = searchEl.value.trim(); renderGrid(); });
|
|
}
|
|
|
|
renderCats();
|
|
renderSubs();
|
|
renderGrid();
|
|
}
|
|
|
|
// ---------- 论坛 ----------
|
|
async function loadForum(){
|
|
const threadsEl = document.getElementById('threads');
|
|
const catsEl = document.querySelector('#forum .f-cats');
|
|
if (!threadsEl) return;
|
|
try {
|
|
const list = await API.threads();
|
|
if (!list || !list.length){ threadsEl.innerHTML = '<div class="th"><span class="tt">暂无主题</span></div>'; return; }
|
|
// 分类筛选
|
|
const cats = [];
|
|
list.forEach(t => { if (t.category && cats.indexOf(t.category) < 0) cats.push(t.category); });
|
|
const catLabels = {};
|
|
list.forEach(t => { if (t.category) catLabels[t.category] = t.category_label || t.category; });
|
|
if (catsEl){
|
|
catsEl.innerHTML = `<div class="ch active" data-fcat="all">全部</div>` +
|
|
cats.map(c => `<div class="ch" data-fcat="${c}">${catLabels[c] || c}</div>`).join('');
|
|
catsEl.querySelectorAll('.ch').forEach(b => b.addEventListener('click', () => {
|
|
catsEl.querySelectorAll('.ch').forEach(x => x.classList.remove('active'));
|
|
b.classList.add('active');
|
|
const c = b.dataset.fcat;
|
|
threadsEl.querySelectorAll('.th').forEach(t => { t.style.display = (c === 'all' || t.dataset.fcat === c) ? '' : 'none'; });
|
|
}));
|
|
}
|
|
threadsEl.innerHTML = list.map(t =>
|
|
`<div class="th" data-fcat="${t.category || 'all'}">` +
|
|
(t.pinned ? `<span class="pin">置顶</span>` : '') +
|
|
`<span class="tt">${t.name}</span>` +
|
|
`<span class="meta"><span class="rp">${(t.replies || []).length}</span> 回复 · ${t.author_name || '匿名'}</span></div>`
|
|
).join('');
|
|
} catch (e) { /* 保留静态兜底内容 */ }
|
|
}
|
|
|
|
// ---------- 公告 ----------
|
|
async function loadAnnounce(){
|
|
const el = document.querySelector('#announce .notes');
|
|
if (!el) return;
|
|
try {
|
|
const list = await API.announcements();
|
|
if (!list || !list.length){ el.innerHTML = '<div class="note"><div class="body">暂无公告</div></div>'; return; }
|
|
el.innerHTML = list.map((n, i) => {
|
|
// 封面图:后台返回 image 字段则铺满封面;否则渐变占位(图片位从后台取)
|
|
const cover = n.image
|
|
? `<div class="note-cover" style="background-image:url('${n.image}')"></div>`
|
|
: `<div class="note-cover empty"></div>`;
|
|
const title = n.title ? `<div class="note-title">${n.title}</div>` : '';
|
|
const feat = (i === 0) ? ' feat' : '';
|
|
return `<div class="note${n.pinned ? ' pin' : ''}${feat}">
|
|
${cover}
|
|
<div class="note-body">
|
|
<div class="date">${n.date || ''}</div>
|
|
${title}
|
|
<div class="body">${n.body_html || ''}</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
} catch (e) { /* 保留静态兜底内容 */ }
|
|
}
|
|
|
|
// ---------- 剧情(小说阅读器:左侧目录 + 右侧正文) ----------
|
|
async function loadStory(){
|
|
const tocList = document.getElementById('storyTocList');
|
|
const reader = document.getElementById('storyReader');
|
|
if (!tocList || !reader) return;
|
|
let data;
|
|
try { data = await API.story(); }
|
|
catch (e) { tocList.innerHTML = '<div class="toc-loading">目录加载失败</div>'; return; }
|
|
const volumes = (data && data.volumes) || [];
|
|
if (!volumes.length){ tocList.innerHTML = '<div class="toc-loading">暂无内容</div>'; return; }
|
|
|
|
const volNameByCh = {};
|
|
let firstCid = null;
|
|
const html = volumes.map(v => {
|
|
const chs = (v.chapters || []).map(c => {
|
|
volNameByCh[c.id] = v.name;
|
|
if (firstCid === null) firstCid = c.id;
|
|
return '<button class="toc-ch" data-cid="' + c.id + '">' + c.name + '</button>';
|
|
}).join('');
|
|
return '<div class="toc-vol"><div class="vname">' + v.name + '</div>' +
|
|
(v.intro ? '<div class="vintro">' + v.intro + '</div>' : '') + chs + '</div>';
|
|
}).join('');
|
|
tocList.innerHTML = html;
|
|
|
|
async function openChapter(cid){
|
|
tocList.querySelectorAll('.toc-ch').forEach(b => b.classList.toggle('active', String(b.dataset.cid) === String(cid)));
|
|
reader.innerHTML = '<div class="reader-loading">加载中…</div>';
|
|
try {
|
|
const d = await API.storyChapter(cid);
|
|
if (!d || d.error){ reader.innerHTML = '<div class="reader-loading">' + ((d && d.error) || '章节加载失败') + '</div>'; return; }
|
|
reader.innerHTML =
|
|
'<div class="reader-inner"><div class="r-vol">' + (volNameByCh[cid] || '') + '</div>' +
|
|
'<h1>' + d.name + '</h1><div class="body">' + (d.body_html || '<p>(暂无正文)</p>') + '</div></div>';
|
|
reader.scrollTop = 0;
|
|
} catch (e) { reader.innerHTML = '<div class="reader-loading">章节加载失败</div>'; }
|
|
}
|
|
|
|
tocList.querySelectorAll('.toc-ch').forEach(b => b.addEventListener('click', () => openChapter(b.dataset.cid)));
|
|
if (firstCid !== null) openChapter(firstCid);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadPages();
|
|
loadRules();
|
|
loadCodex();
|
|
loadForum();
|
|
loadAnnounce();
|
|
loadStory();
|
|
});
|