// 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
← 返回档案墙 / BACK TO ARCHIVE
VOL. XII · ISSUE 04 · 2026 · 阅卷号 {ARTICLE_ID}
); // 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 (
← 档案墙 {next ? ( 下一卷 · {next.id} → ) : ( 返回首页 → )}
); }; const Breadcrumb = ({ post }) => (
首页 / ARCHIVE {" "} {m(post,'da_category','档案')} · {m(post,'da_cat_en','FILE')}{" "} {" "} {ARTICLE_ID} · {stripTags(post.title.rendered)}
); 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 ? (
{stripTags(post.title.rendered)}
+ EVIDENCE · {ARTICLE_ID}
) : ( )}
{m(post,'da_cover_caption', '封面证物 · ' + ARTICLE_ID)}
); }; const FactSheet = ({ post }) => { const cell = (label, value, red) => (
{label}
{value}
); 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 ? ( <>