feat(定时任务): 新增第七个 HTML 模板 no.7(只展示)

- no.7.html(605KB)复制进 templates/no7/;数据存于 <script id="PAGE_DATA"
  type="application/json"> 元素(同 no.4 类元素注入),当前引擎只支持 const VAR= 行注入,
  故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.7
- 测试覆盖 no.7 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-24 18:13:38 +08:00
1 parent 4f62703477
commit 60029ae3dc
2 files changed
+231

No files matched your search

@@ -175,6 +175,15 @@ TEMPLATES: tuple[TemplateSpec, ...] = (
# 故暂注册为只展示;接转换需放宽行注入正则。
convertible=False,
),
TemplateSpec(
id="no7",
name="no.7",
description="",
file="no7/no7.html",
# 数据存于 <script id="PAGE_DATA" type="application/json"> 元素(同 no.4 类元素注入),
# 当前引擎只支持 const VAR= 行注入,故暂注册为只展示;接转换需扩展元素注入。
convertible=False,
),
)
_TEMPLATES_BY_ID: dict[str, TemplateSpec] = {t.id: t for t in TEMPLATES}
@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>台湾民防与全社会防卫韧性知识展示平台 v3 横向Tab版</title>
<style>
:root{
--red-950:#4f0b16;--red-900:#68111f;--red-800:#7f1829;--red-700:#a11f35;--red-600:#bb3148;--red-100:#fff0f2;--red-50:#fff7f8;
--ink:#1f2937;--muted:#667085;--line:#e5e7eb;--soft:#f7f8fb;--card:#fff;--green:#166534;--orange:#9a3412;--blue:#1d4ed8;
--shadow:0 18px 45px rgba(38, 23, 31, .18);--radius:22px;
}
*{box-sizing:border-box} html{scroll-behavior:smooth}
body{margin:0;font-family:"Microsoft YaHei","PingFang SC","Noto Sans CJK SC",Arial,sans-serif;background:radial-gradient(circle at 10% 0%, #d66275 0, transparent 30%),linear-gradient(135deg,var(--red-950),var(--red-700));color:var(--ink);min-height:100vh;}
.page{max-width:1500px;margin:0 auto;padding:26px 22px 48px}.hero{color:#fff;border-radius:30px;border:1px solid rgba(255,255,255,.24);background:linear-gradient(135deg,rgba(255,255,255,.15),rgba(255,255,255,.06));box-shadow:var(--shadow);padding:24px 26px}.hero-top{display:flex;justify-content:space-between;gap:20px;align-items:flex-start}.hero h1{margin:0 0 10px;font-size:31px;letter-spacing:.4px}.hero p{margin:0;line-height:1.72;color:rgba(255,255,255,.92);max-width:1000px}.badge-line{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}.badge{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:12px;font-weight:700;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.22);color:#fff}.safe{margin-top:14px;padding:12px 14px;border-radius:16px;background:rgba(255,255,255,.13);font-size:13px;line-height:1.65;color:#fff}.layout{margin-top:18px;display:grid;grid-template-columns:1fr;gap:18px}.sidebar{position:relative;top:0;align-self:stretch;background:#fff;border:1px solid rgba(255,255,255,.35);border-radius:var(--radius);box-shadow:var(--shadow);padding:14px 16px}.nav-title{font-size:13px;color:var(--muted);font-weight:900;margin:2px 4px 10px;letter-spacing:.08em}#tabs{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.tab{width:auto;min-width:138px;border:1px solid #f0d7dc;background:#fff;color:var(--ink);border-radius:999px;padding:11px 14px;margin:0;cursor:pointer;text-align:center;font-weight:900;display:flex;align-items:center;justify-content:center;gap:8px;white-space:nowrap;box-shadow:0 4px 12px rgba(15,23,42,.035)}.tab:hover{background:var(--red-50);border-color:#ffd8df;transform:translateY(-1px)}.tab.active{background:var(--red-700);color:#fff;border-color:var(--red-700);box-shadow:0 8px 18px rgba(161,31,53,.22)}.tab-count{font-size:11px;border-radius:999px;padding:2px 8px;background:#f3f4f6;color:#374151}.tab.active .tab-count{background:rgba(255,255,255,.2);color:#fff}.main{min-width:0}.panel{background:#fff;border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;border:1px solid rgba(255,255,255,.45)}.toolbar{display:grid;grid-template-columns:minmax(250px,1fr) 180px 180px 160px 140px;gap:11px;padding:16px;border-bottom:1px solid var(--line);background:#fff}.input,.select,.viewbtn{width:100%;border:1px solid var(--line);border-radius:14px;padding:11px 12px;font-size:14px;outline:none;background:#fff}.viewbtn{font-weight:800;cursor:pointer;color:var(--red-700)}.input:focus,.select:focus{border-color:var(--red-600);box-shadow:0 0 0 3px rgba(187,49,72,.12)}.content{padding:20px;background:#f7f8fa;min-height:670px}.section-title{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:16px}.section-title h2{margin:0 0 8px;font-size:23px;color:#111827}.section-title .desc{max-width:760px;text-align:right;color:var(--muted);font-size:13px;line-height:1.65}.count{font-size:13px;color:var(--muted)}.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:13px;margin-bottom:16px}.metric{background:#fff;border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 8px 20px rgba(15,23,42,.04)}.metric .label{font-size:13px;color:var(--muted);margin-bottom:8px}.metric .value{font-size:31px;font-weight:900;color:var(--red-700)}.metric .sub{font-size:12px;color:#697386;line-height:1.48;margin-top:8px}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}.box{background:#fff;border:1px solid var(--line);border-radius:18px;padding:16px}.box h3{margin:0 0 12px;font-size:16px}.bar-row{display:grid;grid-template-columns:175px 1fr 44px;gap:9px;align-items:center;margin:9px 0;font-size:13px}.bar{height:10px;border-radius:999px;background:#f1f5f9;overflow:hidden}.bar span{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--red-700),#e96f82)}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.card{position:relative;background:#fff;border:1px solid var(--line);border-radius:20px;padding:16px;cursor:pointer;transition:.18s;min-height:230px;display:flex;flex-direction:column;overflow:hidden}.card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:5px;background:var(--red-700);opacity:.88}.card:hover{transform:translateY(-2px);box-shadow:0 14px 30px rgba(15,23,42,.11);border-color:#e8b7c0}.card h3{margin:0 0 8px;padding-left:2px;font-size:17px;line-height:1.35;color:#111827}.card .subtitle{font-size:13px;color:#4b5563;line-height:1.55;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.mini-fields{margin:12px 0 8px;border-top:1px dashed #edf0f3;padding-top:10px}.mini{display:grid;grid-template-columns:72px 1fr;gap:8px;font-size:12.5px;line-height:1.45;margin:5px 0}.mini .mk{color:#7a8291;font-weight:800}.mini .mv{color:#374151;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto;padding-top:12px}.chip{font-size:12px;border:1px solid #ead1d6;background:var(--red-50);color:var(--red-800);border-radius:999px;padding:4px 8px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chip.gray{border-color:#e5e7eb;background:#f9fafb;color:#4b5563}.chip.high{border-color:#fecaca;background:#fef2f2;color:#991b1b}.chip.mid{border-color:#fed7aa;background:#fff7ed;color:#9a3412}.chip.ok{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.table-wrap{overflow:auto;border-radius:18px;border:1px solid var(--line);background:#fff}table{width:100%;border-collapse:collapse;min-width:880px}th,td{padding:12px;border-bottom:1px solid var(--line);font-size:13px;text-align:left;vertical-align:top}th{background:#fbfbfc;color:#374151;font-weight:900;position:sticky;top:0}tr:last-child td{border-bottom:0}tr:hover td{background:#fff8f9}.empty{padding:44px;text-align:center;color:var(--muted);background:#fff;border:1px dashed #d1d5db;border-radius:18px}.modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.60);display:none;align-items:center;justify-content:center;padding:24px;z-index:60}.modal-backdrop.show{display:flex}.modal{width:min(1160px,96vw);max-height:91vh;overflow:auto;background:#fff;border-radius:24px;box-shadow:0 30px 80px rgba(0,0,0,.36)}.modal-header{position:sticky;top:0Line truncated
@media(max-width:1180px){.toolbar{grid-template-columns:1fr 1fr}.metrics{grid-template-columns:repeat(3,1fr)}.cards{grid-template-columns:repeat(2,1fr)}}
@media(max-width:760px){.page{padding:14px}.hero-top{display:block}.hero h1{font-size:23px}.toolbar{grid-template-columns:1fr}.metrics{grid-template-columns:1fr}.cards{grid-template-columns:1fr}.grid-2,.kv-grid,.mini-card-grid,.columns{grid-template-columns:1fr}.section-title{display:block}.section-title .desc{text-align:left;margin-top:8px}.bar-row{grid-template-columns:1fr}.bar-row .bar{height:10px}.modal-backdrop{padding:10px}.modal-header{padding:15px}.modal-body{padding:14px}}
</style>
</head>
<body>
<div class="page">
<header class="hero">
<div class="hero-top">
<div>
<h1 id="appTitle">台湾民防与全社会防卫韧性知识展示平台</h1>
<p id="appDesc"></p>
</div>
</div>
<div class="badge-line" id="heroBadges"></div>
<div class="safe" id="safeNotice"></div>
</header>
<div class="layout">
<aside class="sidebar">
<div class="nav-title">横向栏目导航</div>
<nav id="tabs"></nav>
<div class="view-note">提示:点击任意卡片可进入结构化详情页,查看中文字段、标签、时间线、观点对照、风险边界和来源列表。</div>
</aside>
<main class="main panel">
<div class="toolbar">
<input id="searchInput" class="input" type="search" placeholder="输入关键词:组织、政策、争议、风险、来源……" />
<select id="categoryFilter" class="select"><option value="">全部分类</option></select>
<select id="levelFilter" class="select"><option value="">全部等级/层级</option></select>
<select id="evidenceFilter" class="select"><option value="">全部证据强度</option></select>
<button id="viewToggle" class="viewbtn" type="button">卡片视图</button>
</div>
<section class="content" id="content"></section>
</main>
</div>
<div class="footer-note">安全边界:仅展示公开资料研究、政策法源、组织画像、舆情争议和风险评估;不展示训练动作、敏感设施点位、私人信息、攻击破坏方法、定向影响或骚扰策略。</div>
</div>
<div class="modal-backdrop" id="modalBackdrop">
<article class="modal">
<div class="modal-header">
<div>
<h2 id="modalTitle"></h2>
<p id="modalSub"></p>
<div class="chips" id="modalChips"></div>
</div>
<button class="close" id="modalClose" aria-label="关闭">×</button>
</div>
<div class="modal-body" id="modalBody"></div>
</article>
</div>
<script id="PAGE_DATA" type="application/json">{"meta": {"title": "台湾民防与全社会防卫韧性知识展示平台", "version": "v2.0-rich-display", "generated_at": "2026-06-24", "language": "zh-CN", "description": "增强版页面展示 JSON:中文字段名、分区详情弹框、标签化摘要、时间线、观点对照、传播链和风险卡片。", "safe_display_notice": "本页面用于公开资料研究、政策展示与风险评估,不展示训练操作、敏感设施、个人隐私、定向影响或可用于攻击破坏的信息。", "data_boundary": ["仅展示公开资料研究、政策法源、组织画像、舆情争议和风险评估。", "不展示训练操作、敏感设施点位、个人隐私、攻击破坏方法、定向影响或骚扰策略。"]}, "overview": {"dataset_name": "taiwan_civil_defense_page_overview_metrics", "created_at": "2026-06-24", "last_verified_hint": "部分公共资料核验至2026-06-24;页面上线前应再次核验官方来源。", "core_metrics": {"organization_system_records": 35, "civil_group_profiles": 14, "policy_cases": 12, "controversy_cases": 16, "risk_assessment_items": 18, "source_catalog_items": 16}, "category_distribution": {"organizations_by_category": {"关键基础设施防护编组": 9, "民间训练组织": 8, "地方政府动员体系": 7, "法定民防团队": 6, "企业学校防护编组": 5}, "civil_groups_by_category": {"民防教育训练型": 1, "社会韧性与应急训练型": 1, "地方自训网络枢纽型": 1, "社区韧性与宗教网络型": 1, "去中心化地方自训型": 1, "地方自训组织案例": 1, "灾难医疗与救援型": 1, "人道救援与急救培训型": 1, "宗教慈善与灾害支援型": 1, "宗教社区网络型": 1, "资讯与国际传播韧性型": 1, "资讯韧性与民主研究型": 1, "资安与关键网络韧性型": 1, "全民国防教育型": 1}, "risks_by_level": {"高": 6, "中高": 10, "中": 2}, "policies_by_type": {"法律": 2, "施行细则": 1, "行政规则/办法": 1, "总统府任务编组/政策平台": 1, "演习机制": 1, "公众安全指引": 1, "教育/动员准备机制": 1, "教育法制": 1, "灾害治理法制": 1, "资安/关键系统治理": 1, "地方公众指引/手册体系": 1}}, "top_display_cards": [{"card_title": "组织体系", "value": 35, "subtitle": "法定民防、地方动员、关键设施、企业学校、民间训练五层结构"}, {"card_title": "政策法源", "value": 12, "subtitle": "法律、办法、委员会、演习、安全指引与地方手册"}, {"card_title": "民间组织", "value": 14, "subtitle": "教育训练、社会韧性、人道救援、资讯韧性等组织画像"}, {"card_title": "舆情争议", "value": 16, "subtitle": "学生服勤、军事化争议、资金争议、标签化、人道中立等"}, {"card_title": "风险评估", "value": 18, "subtitle": "国际人道法、隐私、社会心理、关键设施和治理风险"}, {"card_title": "资料来源", "value": 16, "subtitle": "官方法源、总统府资料、国防部资料、教育部澄清、国际人道法与媒体资料"}], "frontend_data_files": ["taiwan_civil_defense_overview_metrics_v1.json", "taiwan_civil_defense_policy_legal_cases_v1.json", "taiwan_civil_defense_organization_system_enriched_v1.json", "taiwan_civil_defense_risk_assessments_v1.json", "taiwan_civil_defense_controversy_propagation_v1.json", "taiwan_civil_groups_activity_funding_v1.json"], "recommended_home_sections": ["统计卡片", "五层组织体系图", "政策时间线", "高风险议题Top列表", "重点争议列表", "来源可信度说明"], "safe_display_notice": "本页面用于公开资料研究、政策展示与风险评估,不展示训练操作、敏感设施、个人隐私、定向影响或可用于攻击破坏的信息。"}, "global_query": {"enabled": true, "query_mode": "client_side_fuzzy_search", "supports": ["关键词搜索", "Tab 内筛选", "证据强度筛选", "风险/层级筛选", "详情弹框查看", "JSON 字段递归展示"]}, "tabs": [{"id": "overview", "label": "首页总览", "description": "展示核心统计、分类分布、重点卡片和安全边界说明。", "record_count": 1, "default_view": "dashboard", "records": [{"dataset_name": "taiwan_civil_defense_page_overview_metrics", "created_at": "2026-06-24", "last_verified_hint": "部分公共资料核验至2026-06-24;页面上线前应再次核验官方来源。", "core_metrics": {"organization_system_records": 35, "civil_group_profiles": 14, "policy_cases": 12, "controversy_cases": 16, "risk_assessment_items": 18, "source_catalog_items": 16}, "category_distribution": {"organizations_by_category": {"关键基础设施防护编组": 9, "民间训练组织": 8, "地方政府动员体系": 7, "法定民防团队": 6, "企业学校防护编组": 5}, "civil_groups_by_category": {"民防教育训练型": 1, "社会韧性与应急训练型": 1, "地方自训网络枢纽型": 1, "社区韧性与宗教网络型": 1, "Line truncated
<script>
const DATA = JSON.parse(document.getElementById('PAGE_DATA').textContent);
const LABELS = DATA.field_labels || {};
const LAYOUTS = DATA.detail_layouts || {};
const state = {activeTab:'overview', query:'', category:'', level:'', evidence:'', view:'cards'};
const $ = s => document.querySelector(s);
const tabsEl=$('#tabs'), contentEl=$('#content'), searchInput=$('#searchInput'), categoryFilter=$('#categoryFilter'), levelFilter=$('#levelFilter'), evidenceFilter=$('#evidenceFilter'), viewToggle=$('#viewToggle');
function esc(s){return String(s ?? '').replace(/[&<>\'\"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));}
function label(k){return LABELS[k] || humanize(k)}
function humanize(k){return String(k).replace(/_/g,' ').replace(/\b\w/g,m=>m.toUpperCase())}
function textify(x){if(x==null)return''; if(['string','number','boolean'].includes(typeof x))return String(x); if(Array.isArray(x))return x.map(textify).join(' '); if(typeof x==='object')return Object.values(x).map(textify).join(' '); return String(x)}
function truncate(s,n=120){s=String(s||'');return s.length>n?s.slice(0,n)+'…':s}
function activeTab(){return DATA.tabs.find(t=>t.id===state.activeTab)||DATA.tabs[0]}
function recordsOf(t){return t.records||[]}
function unique(arr){return [...new Set(arr.filter(Boolean).map(String))]}
function chipClass(v){v=String(v||''); if(/高|重大|严重|high|高争议|易复燃/.test(v)) return 'high'; if(/中|mixed|medium|中高/.test(v)) return 'mid'; if(/低|low|confirmed/.test(v)) return 'ok'; return 'gray'}
function renderChips(arr,limit=12){return (arr||[]).filter(Boolean).slice(0,limit).map(t=>`<span class="chip ${chipClass(t)}">${esc(t)}</span>`).join('')}
function renderIdPills(arr){if(!arr)return '—'; if(!Array.isArray(arr))arr=[arr]; return `<div class="pill-list">${arr.map(x=>`<span class="id-pill">${esc(x)}</span>`).join('')}</div>`}
function init(){
$('#appTitle').textContent=DATA.meta.title;
$('#appDesc').textContent=DATA.meta.description;
$('#safeNotice').textContent=DATA.meta.safe_display_notice || (DATA.meta.data_boundary||[]).join(' ');
const metrics=(DATA.overview.top_display_cards||[]).slice(0,6);
$('#heroBadges').innerHTML=metrics.map(m=>`<span class="badge">${esc(m.card_title)}:${esc(m.value)}</span>`).join('') + `<span class="badge">版本:${esc(DATA.meta.version)}</span>`;
buildTabs(); updateFilters(); render();
}
function buildTabs(){
tabsEl.innerHTML=DATA.tabs.map(t=>`<button class="tab ${t.id===state.activeTab?'active':''}" data-tab="${esc(t.id)}"><span>${esc(t.label)}</span><span class="tab-count">${esc(t.record_count ?? (t.records||[]).length)}</span></button>`).join('');
tabsEl.querySelectorAll('.tab').forEach(btn=>btn.onclick=()=>{state.activeTab=btn.dataset.tab; state.category='';state.level='';state.evidence=''; buildTabs();updateFilters();render();});
}
function updateFilters(){
const tab=activeTab(), recs=recordsOf(tab);
const getVals=id=>tab.filters?.find(f=>f.id===id)?.values || unique(recs.map(r=>r[id]));
fillSelect(categoryFilter,'全部分类',getVals('category'),state.category);
fillSelect(levelFilter,'全部等级/层级',getVals('level'),state.level);
fillSelect(evidenceFilter,'全部证据强度',getVals('evidence'),state.evidence);
const ov=tab.id==='overview'; [categoryFilter,levelFilter,evidenceFilter].forEach(el=>el.disabled=ov); viewToggle.disabled=ov;
}
function fillSelect(el,first,vals,val){el.innerHTML=`<option value="">${first}</option>`+vals.map(v=>`<option value="${esc(v)}">${esc(v)}</option>`).join(''); el.value=val||'';}
function filteredRecords(){
const tab=activeTab(); if(tab.id==='overview')return recordsOf(tab);
const q=state.query.trim().toLowerCase();
return recordsOf(tab).filter(r=>(!q||textify(r).toLowerCase().includes(q))&&(!state.category||String(r.category||'')===state.category)&&(!state.level||String(r.level||'')===state.level)&&(!state.evidence||String(r.evidence||'')===state.evidence));
}
function render(){
buildTabs(); updateFilters(); const tab=activeTab(); if(tab.id==='overview') return renderOverview();
const recs=filteredRecords();
contentEl.innerHTML=`<div class="section-title"><div><h2>${esc(tab.label)}</h2><div class="count">当前显示 ${recs.length} / ${recordsOf(tab).length} 条记录</div></div><div class="desc">${esc(tab.description||'')}</div></div>` +
(recs.length ? (state.view==='cards'?`<div class="cards">${recs.map(cardHTML).join('')}</div>`:tableHTML(recs)) : `<div class="empty">没有匹配结果。请调整关键词或筛选条件。</div>`);
contentEl.querySelectorAll('[data-open-id]').forEach(el=>el.onclick=()=>openModal(el.dataset.openId));
}
function cardFieldsForTab(tabId){
return {
organizations:['类型','主管/协调体系','主要功能','证据强度'],
policies:['类型','状态','主管机关','风险提示'],
civil_groups:['分类','组织类型','核心功能','风险提示'],
controversies:['时间范围','状态','相关组织','风险等级'],
risks:['风险类型','风险等级','影响对象','触发条件']
}[tabId] || [];
}
function cardHTML(r){
const fields=cardFieldsForTab(state.activeTab);
const mini=fields.map(k=>`<div class="mini"><div class="mk">${esc(k)}</div><div class="mv">${esc(truncate(textify((r.summary||{})[k]),90))}</div></div>`).join('');
return `<article class="card" data-open-id="${esc(r.id)}"><h3>${esc(r.title)}</h3><div class="subtitle">${esc(truncate(r.subtitle,185))}</div><div class="mini-fields">${mini}</div><div class="chips">${renderChips(r.tags||[],5)}${r.evidence?`<span class="chip ${chipClass(r.evidence)}">证据:${esc(r.evidence)}</span>`:''}</div></article>`;
}
function tableHTML(recs){
return `<div class="table-wrap"><table><thead><tr><th>编号</th><th>标题</th><th>分类</th><th>状态</th><th>层级/等级</th><th>证据强度</th><th>摘要</th></tr></thead><tbody>${recs.map(r=>`<tr data-open-id="${esc(r.id)}"><td>${esc(r.id)}</td><td><b>${esc(r.title)}</b></td><td>${esc(r.category)}</td><td>${esc(r.status)}</td><td>${esc(r.level)}</td><td>${esc(r.evidence)}</td><td>${esc(truncate(r.subtitle,160))}</td></tr>`).join('')}</tbody></table></div>`
}
function renderOverview(){
const ov=DATA.overview, cards=ov.top_display_cards||[], dist=ov.category_distribution||{};
contentEl.innerHTML=`<div class="section-title"><div><h2>首页总览</h2><div class="count">生成日期:${esc(ov.created_at)}|${esc(ov.last_verified_hint)}</div></div><div class="desc">展示核心统计、分类分布、重点卡片和安全边界说明。</div></div>
<div class="metrics">${cards.map(c=>`<div class="metric"><div class="label">${esc(c.card_title)}</div><div class="value">${esc(c.value)}</div><div class="sub">${esc(c.subtitle)}</div></div>`).join('')}</div>
<div class="grid-2">${distributionBox('组织分类分布',dist.organizations_by_category)}${distributionBox('风险等级分布',dist.risks_by_level)}${distributionBox('政策类型分布',dist.policies_by_type)}${distributionBox('民间组织分类分布',dist.civil_groups_by_category)}</div>
<div class="box" style="margin-top:14px"><h3>推荐首页区块</h3>${renderValue(ov.recommended_home_sections||[])}</div>`;
}
function distributionBox(title,obj){obj=obj||{}; const max=Math.max(...Object.values(obj).map(Number),1); const rows=Object.entries(obj).map(([k,v])=>`<div class="bar-row"><div>${esc(k)}</div><div class="bar"><span style="width:${Math.max(4,Number(v)/max*100)}%"></span></div><div>${esc(v)}</div></div>`).join('');return `<div class="box"><h3>${esc(title)}</h3>${rows||'<div class="empty">暂无数据</div>'}</div>`}
function openModal(id){
const tab=activeTab(); const rec=recordsOf(tab).find(r=>String(r.id)===String(id)); if(!rec)return;
$('#modalTitle').textContent=rec.title||rec.id; $('#modalSub').textContent=rec.subtitle||''; $('#modalChips').innerHTML=renderChips(rec.tags||[],12);
$('#modalBody').innerHTML=renderDetail(tab.id,rec.detail||rec); $('#modalBackdrop').classList.add('show');
}
function renderDetail(tabId, detail){
const layout=LAYOUTS[tabId]||[]; const used=new Set();
const sections=layout.map(sec=>{const body=sec.keys.map(k=>{used.add(k); return renderField(k,detail[k],tabId)}).filter(Boolean).join(''); return body?section(sec.title,body):''}).join('');
const extras=Object.keys(detail).filter(k=>!used.has(k) && !/^_/.test(k));
const extraHtml=extras.length?section('其他补充信息',extras.map(k=>renderField(k,detail[k],tabId)).filter(Boolean).join('')):'';
return sections + extraHtml;
}
function section(title,body){return `<section class="detail-section"><div class="section-head"><h3>${esc(title)}</h3></div><div class="section-content">${body}</div></section>`}
function renderField(k,v,tabId){
if(v==null || v==='' || (Array.isArray(v)&&!v.length)) return '';
const val = specialized(k,v,tabId) || renderValue(v,k);
return `<div class="kv"><div class="k">${esc(label(k))}</div><div class="v">${val}</div></div>`;
}
function specialized(k,v,tabId){
if(k==='source_url' && v) return `<a class="source-link" href="${esc(v)}" target="_blank" rel="noopener">${esc(v)}</a>`;
if(['related_policy_ids','related_risk_ids','related_org_ids','related_controversy_ids','controversy_links','frontend_tags','source_ids'].includes(k)) return renderIdPills(v);
if(k==='sources' && Array.isArray(v)) return v.map(s=>typeof s==='string'?`<span class="id-pill">${esc(s)}</span>`:`<a class="source-link" href="${esc(s.url||'#')}" target="_blank" rel="noopener"><b>${esc(s.title||s.url||'资料来源')}</b><br>${esc(s.source_type||'')} ${s.confidence?`|可信度:${esc(s.confidence)}`:''}</a>`).join('');
if(k==='public_key_persons' && Array.isArray(v)) return `<div class="mini-card-grid">${v.map(p=>`<div class="mini-card"><div class="mini-title">${esc(p.name)}</div><div>${esc(p.public_role||'')}</div><div class="view-note">${esc(p.notes||'')}</div></div>`).join('')}</div>`;
if(k==='training_topics' && Array.isArray(v)) return `<div class="mini-card-grid">${v.map(t=>`<div class="mini-card"><div class="mini-title">${esc(t.topic)}</div><div>${esc(t.public_description||'')}</div><div class="chips"><span class="chip ${chipClass(t.operational_sensitivity)}">敏感度:${esc(t.operational_sensitivity||'')}</span></div></div>`).join('')}</div>`;
if(k==='policy_links' && Array.isArray(v)) return `<div class="mini-card-grid">${v.map(p=>`<div class="mini-card"><div class="mini-title">${esc(p.policy_or_agency)}</div><div>${esc(p.relationship_type||'')}</div><div class="view-note">${esc(p.evidence||'')}</div><div class="chips"><span class="chip ${chipClass(p.evidence_strength)}">${esc(p.evidence_strength||'')}</span></div></div>`).join('')}</div>`;
if(k==='controversies' && Array.isArray(v)) return `<div class="timeline">${v.map(c=>`<div class="tl-item"><div class="tl-date">${esc(c.date||c.event||'争议')}</div><div class="tl-text"><b>${esc(c.event||'')}</b><br>${esc(c.issue||'')}${c.public_response?`<br>公开回应:${esc(c.public_response)}`:''}</div><div class="chips">${c.risk_tag?`<span class="chip high">${esc(c.risk_tag)}</span>`:''}</div></div>`).join('')}</div>`;
if(k==='factual_anchors' && Array.isArray(v)) return `<div class="timeline">${v.map(a=>`<div class="tl-item"><div class="tl-date">${esc(a.date||'时间未明')}</div><div class="tl-text">${esc(a.fact||'')}</div><div>${renderIdPills(a.source_ids||[])}</div><div class="chips"><span class="chip ${chipClass(a.evidence_status)}">${esc(a.evidence_status||'')}</span></div></div>`).join('')}</div>`;
if(k==='position_map' && typeof v==='object') return `<div class="columns">${Object.entries(v).map(([kk,vv])=>`<div class="mini-card"><div class="mini-title">${esc(label(kk))}</div><div>${renderValue(vv,kk)}</div></div>`).join('')}</div>`;
if(k==='public_opinion_signals' && typeof v==='object') return `<div class="mini-card-grid">${Object.entries(v).map(([kk,vv])=>`<div class="mini-card"><div class="mini-title">${esc(label(kk))}</div>${Array.isArray(vv)?renderChips(vv,30):renderValue(vv,kk)}</div>`).join('')}</div>`;
if(k==='propagation' && typeof v==='object') return renderPropagation(v);
if(k==='activity_funding' && typeof v==='object') return renderActivityFunding(v);
if(k==='risk_assessment' && typeof v==='object') return `<div class="mini-card-grid">${Object.entries(v).map(([kk,vv])=>`<div class="mini-card"><div class="mini-title">${esc(label(kk))}</div>${renderValue(vv,kk)}</div>`).join('')}</div>`;
return null;
}
function renderPropagation(v){
const chain=v.propagation_chain||[]; const chainHtml=Array.isArray(chain)?`<div class="timeline">${chain.map(c=>`<div class="tl-item"><div class="tl-date">${esc(c.stage||c.date||'阶段')}</div><div class="tl-text">${renderValue(c)}</div></div>`).join('')}</div>`:renderValue(chain);
return `<div class="mini-card"><div class="mini-title">传播链</div>${chainHtml}</div><div class="mini-card"><div class="mini-title">可能平台扩散模式</div>${renderValue(v.likely_platform_pattern)}</div><div class="mini-card"><div class="mini-title">关联风险</div>${renderIdPills(v.related_risk_ids||[])}</div><div class="mini-card"><div class="mini-title">展示边界</div>${esc(v.safe_display_notes||'')}</div>`;
}
function renderActivityFunding(v){
const keys=['activity_records','funding_records','cooperation_records','media_exposure_records','related_controversy_ids','related_risk_ids','missing_fields_to_fill_next','safe_display_notes'];
return `<div class="mini-card-grid">${keys.filter(k=>v[k]!=null).map(k=>`<div class="mini-card"><div class="mini-title">${esc(label(k))}</div>${renderValue(v[k],k)}</div>`).join('')}</div>`;
}
function renderValue(v,k){
if(v==null || v==='') return '—';
if(typeof v==='string' || typeof v==='number' || typeof v==='boolean'){
const s=String(v);
if(/^https?:\/\//.test(s)) return `<a class="source-link" href="${esc(s)}" target="_blank" rel="noopener">${esc(s)}</a>`;
return esc(s);
}
if(Array.isArray(v)){
if(!v.length)return '—';
if(v.every(x=>typeof x==='string'||typeof x==='number')){
const asIds=v.every(x=>/^[A-Z]{2,}_|^TC_|^CON_|^POL_|^ORG_|^RISK_|^SRC_/.test(String(x)));
return asIds ? renderIdPills(v) : `<ul class="list">${v.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`;
}
return `<div class="mini-card-grid">${v.map(item=>`<div class="mini-card">${renderValue(item)}</div>`).join('')}</div>`;
}
if(typeof v==='object'){
return Object.entries(v).map(([kk,vv])=>`<div class="kv"><div class="k">${esc(label(kk))}</div><div class="v">${renderValue(vv,kk)}</div></div>`).join('');
}
return esc(v);
}
$('#modalClose').onclick=()=>$('#modalBackdrop').classList.remove('show');
$('#modalBackdrop').onclick=e=>{if(e.target.id==='modalBackdrop')$('#modalBackdrop').classList.remove('show')};
searchInput.oninput=e=>{state.query=e.target.value;render()};
categoryFilter.onchange=e=>{state.category=e.target.value;render()};
levelFilter.onchange=e=>{state.level=e.target.value;render()};
evidenceFilter.onchange=e=>{state.evidence=e.target.value;render()};
viewToggle.onclick=()=>{state.view=state.view==='cards'?'table':'cards'; viewToggle.textContent=state.view==='cards'?'卡片视图':'表格视图'; render()};
document.addEventListener('keydown',e=>{if(e.key==='Escape')$('#modalBackdrop').classList.remove('show')});
init();
</script>
</body>
</html>