// Article page v3 — full dossier layout, all data from WP REST
// /article/?id=DA-XXXX → fetch slug → render original dossier design dynamically.
const qs_ = new URLSearchParams(location.search);
const pathMatch_ = location.pathname.match(/\/article\/(DA-[A-Za-z0-9-]+)/i);
const ARTICLE_ID = (pathMatch_ ? pathMatch_[1] : (qs_.get('id') || 'DA-0884-C')).toUpperCase();
const ARTICLE_SLUG = ARTICLE_ID.toLowerCase();
const m = (post, k, f = "") => {
const v = post && post.meta && post.meta[k];
return v !== undefined && v !== null && v !== "" ? v : f;
};
const mjson = (post, k) => {
let raw = m(post, k, "");
if (!raw) return [];
// Meta is stored as base64(json) to survive WP slash sanitization of embedded quotes.
if (!raw.trim().startsWith('[') && !raw.trim().startsWith('{')) {
try { raw = decodeURIComponent(escape(atob(raw))); } catch { /* not b64, try raw */ }
}
try { return JSON.parse(raw); } catch { return []; }
};
const stripTags = (h) => (h || "").replace(/<[^>]+>/g, "").trim();
const Header = ({ post }) => (
// CLASSIFIED · 仅限内部阅览 · {m(post,'da_classification','PUBLIC')} //
COPY 001 / 001
);
// Article page uses the shared SiteNav (with hamburger + mobile menu) from chrome.jsx.
const Nav = () => ;
// Fixed bottom bar on mobile: back to archive + previous/next case.
// On desktop this is hidden by the .da-sticky-cta CSS rule.
const MobileStickyCTA = ({ post }) => {
const [, force] = React.useState(0);
React.useEffect(() => {
const h = () => force(v => v + 1);
window.addEventListener("da:data-ready", h);
// tag body so the global CSS can reserve bottom padding
document.body.classList.add("da-article");
return () => {
window.removeEventListener("da:data-ready", h);
document.body.classList.remove("da-article");
};
}, []);
const cid = ARTICLE_ID;
const all = (window.CASES || []).slice().sort((a,b) => (b.published_ts||0) - (a.published_ts||0));
const idx = all.findIndex(c => (c.id || "").toUpperCase() === cid);
const next = idx >= 0 && idx < all.length - 1 ? all[idx + 1] : null;
return (
);
};
const Breadcrumb = ({ post }) => (
);
const TitleBlock = ({ post }) => {
const feat = post._embedded && post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0];
const imgUrl = feat && feat.source_url;
return (
▸ {m(post,'da_cat_en','FILE')} / {m(post,'da_category','档案')}
FILE № {ARTICLE_ID}
{m(post,'da_date','—')}
— {m(post,'da_title_en','')}
归档 · {m(post,'da_filed','—')}
复审 · {m(post,'da_reviewed','—')}
编辑 · {m(post,'da_editor','档案员 06')}
字数 · {m(post,'da_word_count','—')}
{imgUrl ? (
+ EVIDENCE · {ARTICLE_ID}
) : (
)}
{m(post,'da_cover_caption', '封面证物 · ' + ARTICLE_ID)}
);
};
const FactSheet = ({ post }) => {
const cell = (label, value, red) => (
);
const cred = parseInt(m(post,'da_credibility','3'),10) || 3;
const evidence = mjson(post,'da_evidence');
const related = mjson(post,'da_related');
return (
▸ 案件基本信息 / CASE FACT SHEET
本页所有数据经 §VII 条款审核 · 部分精度降级
{cell("发生时间 / PERIOD", m(post,'da_date','—'))}
{cell("结案时间 / CLOSED", m(post,'da_date_closed','—'))}
{cell("事发地点 / LOCATION", m(post,'da_loc_name', m(post,'da_loc','—')))}
{cell("精确坐标 / COORDINATES", m(post,'da_loc_coord','—'))}
{cell("案件状态 / STATUS", m(post,'da_status','—'), true)}
{cell("目击 / 涉案 / WITNESSES", m(post,'da_witnesses','—'))}
可信度评估 / CREDIBILITY
{cred}/5 · 经公开资料交叉确认
证物数量 / EVIDENCE
{evidence.length || '—'} 项选录
相关案件 / LINKED
{related.length || 0} 卷 · 见文末
);
};
// Parse post.content.rendered into sectioned body. Splits on boundaries.
// Strips any embedded tags — inline images now come from da_inline_images meta.
function parseBodySections(html) {
if (!html) return [];
// Strip all ... blocks from body HTML
html = html.replace(/]*>[\s\S]*?<\/figure>/gi, '');
const parser = new DOMParser();
const doc = parser.parseFromString('' + html + '
', 'text/html');
const root = doc.body.firstChild;
const out = [];
let cur = null;
for (const node of Array.from(root.childNodes)) {
if (node.nodeType === 1 && node.tagName === 'H2') {
if (cur) out.push(cur);
cur = { heading: node.textContent.trim(), html: '' };
} else {
const h = node.nodeType === 1 ? node.outerHTML : (node.textContent || '');
if (!cur) { cur = { heading: '', html: '' }; }
cur.html += h;
}
}
if (cur) out.push(cur);
return out;
}
// Parse "40.7°N, 116.4°E (±12 km)" → {lat, lon}
function parseCoord(s) {
if (!s) return null;
const m = s.match(/(-?[\d.]+)\s*°?\s*([NS])?[,,\s]+\s*(-?[\d.]+)\s*°?\s*([EW])?/i);
if (!m) return null;
let lat = parseFloat(m[1]); if ((m[2]||'').toUpperCase() === 'S') lat = -lat;
let lon = parseFloat(m[3]); if ((m[4]||'').toUpperCase() === 'W') lon = -lon;
if (isNaN(lat) || isNaN(lon)) return null;
return { lat, lon };
}
const LocationMap = ({ locName, locCoord }) => {
const c = parseCoord(locCoord);
// Zoom span in degrees — small bbox for city-level, wider if coarse
const pad = 0.08;
const osmSrc = c
? `https://www.openstreetmap.org/export/embed.html?bbox=${c.lon - pad*2},${c.lat - pad},${c.lon + pad*2},${c.lat + pad}&layer=mapnik&marker=${c.lat},${c.lon}`
: null;
const externalLink = c ? `https://www.openstreetmap.org/?mlat=${c.lat}&mlon=${c.lon}#map=10/${c.lat}/${c.lon}` : null;
return (
▸ 事发位置 / LOCATION
{osmSrc ? (
<>
{/* Crosshair overlay on top of map center */}
{/* Corner label */}
OSM · 坐标精度降级
>
) : (
坐标未提供
)}
);
};
// Figure block — reused for inline body images
const Figure = ({ src, caption }) => (
{caption && (
{caption}
)}
);
const ArticleBody = ({ post }) => {
const sections = parseBodySections(post.content.rendered);
const rawEvidence = mjson(post, 'da_evidence');
const sources = mjson(post, 'da_sources');
const inline = mjson(post, 'da_inline_images');
// Cover URL (featured image) for evidence fallback
const feat = post._embedded && post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0];
const coverUrl = feat && feat.source_url;
// Build unique fallback pool from cover + all inline images (dedupe)
const seen = new Set();
const imgPool = [coverUrl, ...inline.map(i => i?.src)].filter(u => {
if (!u || seen.has(u)) return false;
seen.add(u); return true;
});
// Fill empty evidence slots with unique fallbacks only; if pool exhausted, leave null
// (JSX will render EvidencePlaceholder). Prefer explicit e.img; avoid duplicating what's
// already assigned to a prior evidence slot.
const usedImgs = new Set(rawEvidence.map(e => e.img).filter(Boolean));
let fallbackIdx = 0;
const evidence = rawEvidence.map((e) => {
if (e.img) return { ...e };
while (fallbackIdx < imgPool.length && usedImgs.has(imgPool[fallbackIdx])) fallbackIdx++;
if (fallbackIdx < imgPool.length) {
const pick = imgPool[fallbackIdx++];
usedImgs.add(pick);
return { ...e, img: pick };
}
return { ...e, img: null };
});
return (
{sections.map((s, i) => (
{s.heading && (
{s.heading}
)}
{/* Inject inline body images after §1 and §2 */}
{inline[0] && i === 0 && }
{inline[1] && i === 1 && }
))}
{/* § 5 Evidence */}
{evidence.length > 0 && (
§ 5 · 证物选录
{evidence.map((e, i) => (
{e.img ? (
+ {(e.label || '').split(" ")[0].slice(0,16)}
{e.id}
) : (
)}
{e.id}
{e.label}
{e.desc}
))}
)}
{/* § 6 Sources */}
{sources.length > 0 && (
§ 6 · 来源与参考
{sources.map((s, i) => (
[{String(i+1).padStart(2,"0")}]
{s.url ? {s.label} : s.label}
{s.note || ''}
))}
)}
{/* END OF FILE */}
// 本卷宗结束 / END OF FILE
如您掌握本案未公开的信息,可匿名寄往 aidarkfile@gmail.com
);
};
const Sidebar = ({ post, sections }) => {
const timeline = mjson(post, 'da_timeline');
const tags = (m(post,'da_tags','') || '').split(',').map(s => s.trim()).filter(Boolean);
const locName = m(post,'da_loc_name', m(post,'da_loc','—'));
const locCoord = m(post,'da_loc_coord','—');
const toc = sections.map(s => s.heading).filter(Boolean);
const evidence = mjson(post, 'da_evidence');
const sources = mjson(post, 'da_sources');
return (
);
};
const RelatedStrip = ({ post }) => {
const related = mjson(post, 'da_related');
if (!related.length) return null;
return (
);
};
const Footer = () => (
);
const Loading = () => (
▸ LOADING FILE · {ARTICLE_ID} · 正在解密...
);
const NotFound = ({ msg }) => (
▸ ERROR · 档案不存在或已封存
未找到 {ARTICLE_ID}
{msg}
返回档案墙 →
);
const App = () => {
const [post, setPost] = React.useState(null);
const [err, setErr] = React.useState(null);
React.useEffect(() => {
fetch(`/wp-json/wp/v2/posts?slug=${encodeURIComponent(ARTICLE_SLUG)}&_embed`)
.then(r => r.json())
.then(d => {
if (!Array.isArray(d) || d.length === 0) setErr('Slug: ' + ARTICLE_SLUG);
else {
setPost(d[0]);
document.title = stripTags(d[0].title.rendered) + ' · 暗黑收集站';
}
})
.catch(e => setErr(String(e)));
}, []);
if (err) return (
<>
// CLASSIFIED · 仅限内部阅览 // COPY 001 / 001
>
);
if (!post) return ;
return (
<>
>
);
};
ReactDOM.createRoot(document.getElementById("root")).render( );