后端 game_base: - 货币并入 game.rule(rate/note 字段),新增可配置规则类别 game.rule.category(world/faction/character/currency) - 新增「配置」菜单与规则类别配置子项 - /game/api/rules 按类别分组返回,/game/api/currencies 保持兼容 - 列表分组取消等级累计(aggregator=False,适配 Odoo18) - 1.2 pre-migrate 清理旧 game.currency 记录,避免外部ID冲突 前端/官网: - 世界数据大报表(index.html + .wd-* 样式) - 背景 5 板块同步、论坛汉化、过渡资源与 views 拆分(announcement/codex/forum/page/rule/story)
164 lines
6.6 KiB
JavaScript
164 lines
6.6 KiB
JavaScript
// 宇森官网 · 公开页数据打通(图鉴/规则/论坛/公告 从 Odoo 取数)
|
||
// 依赖 assets/game-api.js
|
||
|
||
// ---------- 规则 ----------
|
||
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){ el.innerHTML = '<div class="tdesc">暂无规则数据</div>'; 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 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); });
|
||
if (catsEl){
|
||
catsEl.innerHTML = `<div class="ch active" data-fcat="all">全部</div>` +
|
||
cats.map(c => `<div class="ch" data-fcat="${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) { /* 保留静态兜底内容 */ }
|
||
}
|
||
|
||
// ---------- 公告(卡片 + 弹窗) ----------
|
||
function annEscapeHtml(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
function annFormatDate(d) {
|
||
// "2026-07-09" -> "2026.07.09"
|
||
return d ? String(d).replace(/-/g, '.') : '';
|
||
}
|
||
function annStripHtml(html) {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = html || '';
|
||
return (tmp.textContent || '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
async function loadAnnounce(){
|
||
const root = document.getElementById('announce');
|
||
if (!root) return;
|
||
root.innerHTML = '<div class="loading">加载中…</div>';
|
||
let list = [];
|
||
try {
|
||
list = await API.announcements();
|
||
} catch (e) { /* 落到空态 */ }
|
||
if (!list || !list.length) {
|
||
root.innerHTML = '<div class="announce-empty">暂无公告</div>';
|
||
return;
|
||
}
|
||
root.innerHTML = list.map((n, i) => {
|
||
const pri = n.priority || 'normal';
|
||
const preview = annStripHtml(n.body_html).slice(0, 160);
|
||
return `
|
||
<div class="announce-card priority-${annEscapeHtml(pri)}"
|
||
data-idx="${i}" role="button" tabindex="0"
|
||
aria-label="阅读公告: ${annEscapeHtml(n.name)}">
|
||
<div class="meta-row">
|
||
<span class="date">${annEscapeHtml(annFormatDate(n.date))}</span>
|
||
<span class="dot"></span>
|
||
<span class="badge ${annEscapeHtml(pri)}">${annEscapeHtml(n.priority_label || pri)}</span>
|
||
</div>
|
||
<div class="title">${annEscapeHtml(n.name)}</div>
|
||
<div class="preview">${annEscapeHtml(preview)}</div>
|
||
<div class="more">阅读全文 <span class="arrow">→</span></div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
root.querySelectorAll('.announce-card').forEach(card => {
|
||
const idx = +card.dataset.idx;
|
||
const open = () => openAnnounceModal(list[idx]);
|
||
card.addEventListener('click', open);
|
||
card.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
|
||
});
|
||
});
|
||
}
|
||
|
||
// ---- 弹窗(懒创建一次) ----
|
||
let _annModal = null;
|
||
let _annLastFocus = null;
|
||
function ensureAnnounceModal() {
|
||
if (_annModal) return _annModal;
|
||
_annModal = document.createElement('div');
|
||
_annModal.className = 'announce-modal';
|
||
_annModal.setAttribute('role', 'presentation');
|
||
_annModal.innerHTML = `
|
||
<div class="backdrop" data-close="1" aria-label="关闭"></div>
|
||
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="ann-modal-title">
|
||
<div class="head">
|
||
<div>
|
||
<div class="title" id="ann-modal-title" data-title></div>
|
||
<div class="meta" data-meta></div>
|
||
</div>
|
||
<button class="close" type="button" data-close="1" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="body" data-body></div>
|
||
</div>`;
|
||
document.body.appendChild(_annModal);
|
||
_annModal.addEventListener('click', e => {
|
||
if (e.target.closest('[data-close]')) closeAnnounceModal();
|
||
});
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && _annModal.classList.contains('open')) closeAnnounceModal();
|
||
});
|
||
return _annModal;
|
||
}
|
||
function openAnnounceModal(item) {
|
||
if (!item) return;
|
||
const m = ensureAnnounceModal();
|
||
m.querySelector('[data-title]').textContent = item.name || '';
|
||
m.querySelector('[data-meta]').textContent =
|
||
[annFormatDate(item.date), item.priority_label].filter(Boolean).join(' · ');
|
||
// body_html 来自后台管理员撰写,可信;非管理员输入路径不存在,OK 直接渲染
|
||
m.querySelector('[data-body]').innerHTML = item.body_html || '<p style="color:#556677">(无正文)</p>';
|
||
_annLastFocus = document.activeElement;
|
||
m.classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
// 焦点移入弹窗(关闭按钮)
|
||
const closeBtn = m.querySelector('.close');
|
||
if (closeBtn) closeBtn.focus({ preventScroll: true });
|
||
}
|
||
function closeAnnounceModal() {
|
||
if (!_annModal || !_annModal.classList.contains('open')) return;
|
||
_annModal.classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
if (_annLastFocus && _annLastFocus.focus) _annLastFocus.focus({ preventScroll: true });
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadRules();
|
||
loadForum();
|
||
loadAnnounce();
|
||
});
|