feat(定时任务): 新增第四个 HTML 模板 no.4(只展示)
- no.4.html(3.9MB)复制进 templates/no4/;数据存于 <script id="app-data" type="application/json"> 元素(非 const DATA= 行),当前引擎只支持 const VAR= 行注入, 故先注册为 convertible=False 只展示,转换后续接(需扩展元素注入) - 前端下拉/参考列表从 API 拉取,no.4 自动出现 - 测试覆盖 no.4 存在/只展示/文件可读 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e9c8d1cea2
commit
6a6195f883
2 files changed
+391
No files matched your search
+9
@@ -148,6 +148,15 @@ TEMPLATES: tuple[TemplateSpec, ...] = (
|
||||
# 无 const DATA= 注入点,暂只展示已有数据(后续接入转换后再开)。
|
||||
convertible=False,
|
||||
),
|
||||
TemplateSpec(
|
||||
id="no4",
|
||||
name="no.4",
|
||||
description="",
|
||||
file="no4/no4.html",
|
||||
# 数据存于 <script id="app-data" type="application/json"> 元素(非 const DATA= 行),
|
||||
# 当前引擎只支持 const VAR= 行注入,故暂注册为只展示;接转换需扩展元素注入。
|
||||
convertible=False,
|
||||
),
|
||||
)
|
||||
|
||||
_TEMPLATES_BY_ID: dict[str, TemplateSpec] = {t.id: t for t in TEMPLATES}
|
||||
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>台湾民进党政要与政治生态知识展示平台|三圈人物增强版</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg1:#7f1d1d; --bg2:#450a0a; --card:#fffaf7; --card2:#ffffff; --text:#1f2937; --muted:#6b7280;
|
||||
--red:#b91c1c; --red2:#dc2626; --gold:#b45309; --green:#047857; --blue:#1d4ed8; --purple:#7e22ce;
|
||||
--line:#ead7d0; --soft:#fff1f2; --shadow:0 18px 45px rgba(69,10,10,.22);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:"Microsoft YaHei", "PingFang SC", system-ui, -apple-system, sans-serif;background:radial-gradient(circle at 15% 10%, #b91c1c 0%, var(--bg1) 34%, var(--bg2) 100%);color:var(--text);min-height:100vh}
|
||||
body::before{content:"";position:fixed;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.08),transparent 35%),radial-gradient(circle at 85% 15%,rgba(255,255,255,.10),transparent 28%);pointer-events:none}
|
||||
.header{position:sticky;top:0;z-index:20;backdrop-filter:blur(18px);background:rgba(69,10,10,.72);border-bottom:1px solid rgba(255,255,255,.16);padding:16px 28px;color:#fff}
|
||||
.header-inner{display:flex;gap:18px;align-items:center;justify-content:space-between;max-width:1500px;margin:0 auto}
|
||||
.title h1{margin:0;font-size:22px;letter-spacing:.04em}
|
||||
.title p{margin:5px 0 0;font-size:12px;opacity:.78}
|
||||
.search{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.25);border-radius:999px;padding:8px 12px;min-width:340px}
|
||||
.search input{background:transparent;border:none;outline:none;color:#fff;width:100%;font-size:14px}
|
||||
.search input::placeholder{color:rgba(255,255,255,.68)}
|
||||
.container{max-width:1500px;margin:24px auto 42px;padding:0 22px;position:relative;z-index:1}
|
||||
.dashboard{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));gap:14px;margin-bottom:18px}
|
||||
.stat{background:linear-gradient(180deg,rgba(255,255,255,.94),rgba(255,245,245,.92));border:1px solid rgba(255,255,255,.78);border-radius:18px;padding:14px 16px;box-shadow:var(--shadow)}
|
||||
.stat .k{font-size:12px;color:var(--muted)}.stat .v{font-size:28px;font-weight:800;color:var(--red);margin-top:4px}
|
||||
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 18px}
|
||||
.tab{border:none;border-radius:999px;background:rgba(255,255,255,.9);color:#7f1d1d;padding:10px 15px;font-weight:700;cursor:pointer;box-shadow:0 8px 18px rgba(0,0,0,.08)}
|
||||
.tab.active{background:#7f1d1d;color:#fff;border:1px solid rgba(255,255,255,.5)}
|
||||
.panel{background:rgba(255,250,247,.96);border:1px solid rgba(255,255,255,.7);border-radius:26px;box-shadow:var(--shadow);overflow:hidden}
|
||||
.module-head{padding:22px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:12px;align-items:flex-start;background:linear-gradient(90deg,#fff8f8,#fff)}
|
||||
.module-head h2{margin:0;color:#7f1d1d;font-size:24px}.module-head p{margin:8px 0 0;color:var(--muted);line-height:1.6}
|
||||
.notice{font-size:12px;color:#7f1d1d;background:#fff1f2;border:1px solid #fecdd3;padding:10px 12px;border-radius:14px;max-width:520px}
|
||||
.subtabs{display:flex;gap:8px;flex-wrap:wrap;padding:14px 18px;border-bottom:1px solid var(--line);background:#fff}
|
||||
.subtab{border:1px solid var(--line);background:#fff;border-radius:12px;padding:8px 12px;cursor:pointer;color:#7f1d1d;font-weight:700}
|
||||
.subtab.active{background:#7f1d1d;color:#fff;border-color:#7f1d1d}
|
||||
.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:14px 18px;background:#fffdfb;border-bottom:1px solid var(--line)}
|
||||
.toolbar select,.toolbar input{border:1px solid var(--line);background:#fff;border-radius:12px;padding:8px 10px;min-height:38px;outline:none}
|
||||
.toolbar .hint{color:var(--muted);font-size:12px;margin-left:auto}
|
||||
.content{padding:20px}
|
||||
.circle-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
||||
.circle-col{background:#fff;border:1px solid var(--line);border-radius:22px;padding:15px;min-height:500px}
|
||||
.circle-col h3{margin:0 0 8px;color:#7f1d1d;display:flex;align-items:center;justify-content:space-between}
|
||||
.circle-col .desc{font-size:12px;color:var(--muted);line-height:1.5;margin-bottom:12px}
|
||||
.person-card{background:linear-gradient(180deg,#fff,#fff7f7);border:1px solid #f3d5d5;border-radius:16px;padding:12px;margin:10px 0;cursor:pointer;transition:.16s transform,.16s box-shadow}
|
||||
.person-card:hover{transform:translateY(-2px);box-shadow:0 12px 24px rgba(127,29,29,.14)}
|
||||
.person-card .name{font-weight:800;font-size:16px;color:#7f1d1d;display:flex;justify-content:space-between;gap:8px}
|
||||
.person-card .role{font-size:12px;color:#374151;line-height:1.5;margin-top:5px}
|
||||
.tags{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}
|
||||
.tag{font-size:11px;border-radius:999px;padding:3px 8px;background:#fee2e2;color:#991b1b;border:1px solid #fecaca}
|
||||
.tag.levelA{background:#064e3b;color:#fff;border-color:#064e3b}.tag.levelB{background:#d1fae5;color:#065f46;border-color:#a7f3d0}.tag.levelC{background:#ecfdf5;color:#047857;border-color:#d1fae5}
|
||||
.tag.risk{background:#fee2e2;color:#b91c1c}.tag.blue{background:#dbeafe;color:#1d4ed8;border-color:#bfdbfe}.tag.gold{background:#fef3c7;color:#92400e;border-color:#fde68a}
|
||||
.table-wrap{overflow:auto;background:#fff;border:1px solid var(--line);border-radius:18px}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}th,td{border-bottom:1px solid #f1e2df;padding:10px 11px;text-align:left;vertical-align:top}th{position:sticky;top:0;background:#fff1f2;color:#7f1d1d;z-index:1;white-space:nowrap}td{max-width:320px;line-height:1.45}tr:hover td{background:#fff9f9}
|
||||
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}.card{background:#fff;border:1px solid var(--line);border-radius:18px;padding:14px;cursor:pointer}.card h3{margin:0 0 8px;color:#7f1d1d}.card p{font-size:13px;color:#4b5563;line-height:1.55}
|
||||
.network-layout{display:grid;grid-template-columns:minmax(620px,1.3fr) minmax(350px,.7fr);gap:18px}
|
||||
.network-box{background:#fff;border:1px solid var(--line);border-radius:20px;overflow:hidden;min-height:680px}
|
||||
.network-svg{width:100%;height:680px;display:block;background:linear-gradient(180deg,#fff,#fff8f8)}
|
||||
.node{cursor:pointer}.node text{font-size:11px;paint-order:stroke;stroke:#fff;stroke-width:4px;stroke-linejoin:round;fill:#1f2937}
|
||||
.edge{stroke:#9ca3af;stroke-opacity:.5}.edge.high{stroke:#dc2626;stroke-opacity:.7;stroke-dasharray:5 4}
|
||||
.edge.strong{stroke-width:2.2}.edge.weak{stroke-width:1}
|
||||
.rel-list{background:#fff;border:1px solid var(--line);border-radius:20px;max-height:680px;overflow:auto;padding:12px}
|
||||
.rel-item{border:1px solid #f1d7d7;border-radius:14px;padding:10px;margin-bottom:9px;background:#fffafa;cursor:pointer}
|
||||
.rel-item b{color:#7f1d1d}.rel-item p{margin:6px 0;color:#4b5563;font-size:12px;line-height:1.5}
|
||||
.gallery{display:grid;grid-template-columns:260px 1fr;gap:18px}
|
||||
.map-list{display:flex;flex-direction:column;gap:10px}.map-thumb{background:#fff;border:1px solid var(--line);border-radius:16px;padding:10px;cursor:pointer}.map-thumb.active{border-color:#7f1d1d;background:#fff1f2}.map-thumb img{width:100%;border-radius:10px;border:1px solid #eee}
|
||||
.map-view{background:#fff;border:1px solid var(--line);border-radius:20px;padding:14px;overflow:auto}.map-view img{width:100%;min-width:720px;border-radius:14px;border:1px solid #eee}
|
||||
.legend{background:#fff7ed;border:1px solid #fed7aa;color:#7c2d12;padding:12px;border-radius:14px;font-size:12px;line-height:1.7;margin-top:12px}
|
||||
.timeline{display:grid;gap:12px}.timeline-item{border-left:4px solid #b91c1c;background:#fff;border-radius:14px;padding:12px 14px}
|
||||
.chart{background:#fff;border:1px solid var(--line);border-radius:18px;padding:16px}
|
||||
.chart-row{display:flex;align-items:center;gap:8px;margin:8px 0}.chart-label{width:140px;font-size:12px;color:#4b5563}.bar{height:16px;border-radius:999px;background:#fee2e2;overflow:hidden;flex:1}.bar span{display:block;height:100%;background:#b91c1c;border-radius:999px}
|
||||
.modal{position:fixed;inset:0;background:rgba(17,24,39,.56);display:none;align-items:center;justify-content:center;z-index:50;padding:22px}.modal.show{display:flex}
|
||||
.modal-card{background:#fff;border-radius:24px;max-width:940px;width:100%;max-height:86vh;overflow:auto;box-shadow:0 30px 80px rgba(0,0,0,.34)}.modal-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:18px 22px;border-bottom:1px solid var(--line);background:#fff1f2}.modal-head h2{margin:0;color:#7f1d1d}.close{border:none;background:#7f1d1d;color:#fff;border-radius:999px;width:32px;height:32px;cursor:pointer;font-size:18px}
|
||||
.detail-grid{display:grid;grid-template-columns:180px 1fr;gap:0;border-top:1px solid #f2e0de}.detail-grid .k{background:#fff8f8;color:#7f1d1d;font-weight:700;padding:10px;border-bottom:1px solid #f2e0de}.detail-grid .v{padding:10px;border-bottom:1px solid #f2e0de;line-height:1.6;white-space:pre-wrap;word-break:break-word}
|
||||
.empty{padding:24px;color:var(--muted);text-align:center;background:#fff;border:1px dashed var(--line);border-radius:18px}
|
||||
@media(max-width:1100px){.dashboard{grid-template-columns:repeat(3,1fr)}.circle-grid,.network-layout,.gallery{grid-template-columns:1fr}.search{min-width:260px}}
|
||||
@media(max-width:700px){.dashboard{grid-template-columns:repeat(2,1fr)}.header-inner{flex-direction:column;align-items:stretch}.content{padding:14px}}
|
||||
|
||||
/* 后五栏页面化增强:保持第一栏设计不变,仅增强其他栏目 */
|
||||
.compact-head{align-items:center;padding:18px 24px}.compact-head h2{font-size:23px}.compact-head p{display:none}
|
||||
.module-summary{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.mini-stat{background:#fff1f2;border:1px solid #fecdd3;color:#7f1d1d;border-radius:999px;padding:6px 10px;font-size:12px}.mini-stat b{font-size:16px;margin-right:4px}
|
||||
.rich-cards{grid-template-columns:repeat(auto-fill,minmax(330px,1fr))}.rich-card{cursor:pointer;min-height:190px;display:flex;flex-direction:column;gap:8px;transition:.16s transform,.16s box-shadow;background:linear-gradient(180deg,#fff,#fff9f9)}.rich-card:hover{transform:translateY(-2px);box-shadow:0 14px 28px rgba(127,29,29,.13)}
|
||||
.card-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.card-top h3{line-height:1.35}.pill{white-space:nowrap;border-radius:999px;background:#eef2ff;color:#3730a3;border:1px solid #c7d2fe;padding:3px 9px;font-size:12px;font-weight:700}.risk-pill{background:#fee2e2;color:#991b1b;border-color:#fecaca}.meta-line{font-size:12px;color:#6b7280;line-height:1.5}.summary{font-size:13px;color:#374151;line-height:1.65;margin:4px 0 0}.bullet-box{background:#fff7f7;border:1px solid #f7dada;border-radius:12px;padding:8px 12px;font-size:13px;color:#374151}.bullet-box ul{margin:0;padding-left:18px;line-height:1.65}.label-row{margin-top:6px}.label-row b{font-size:12px;color:#7f1d1d}
|
||||
.mini-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-top:auto}.mini-fields div{background:#fff7f7;border:1px solid #f1d7d7;border-radius:10px;padding:7px 9px}.mini-fields span{display:block;font-size:11px;color:#7f1d1d;font-weight:700;margin-bottom:3px}.mini-fields b{font-size:12px;color:#374151;font-weight:600;line-height:1.5}
|
||||
.risk-bars{display:grid;gap:6px;margin:8px 0}.risk-row{display:grid;grid-template-columns:92px 1fr 36px;align-items:center;gap:8px;font-size:12px;color:#4b5563}.riskbar{height:9px;background:#fee2e2;border-radius:999px;overflow:hidden}.riskbar i{display:block;height:100%;background:linear-gradient(90deg,#f59e0b,#dc2626);border-radius:999px}.risk-row b{text-align:right;color:#7f1d1d}.inline-tags{margin:0}.rich-table td{font-size:12px}.mini-btn{border:1px solid #fecaca;background:#fff1f2;color:#991b1b;border-radius:999px;padding:5px 9px;cursor:pointer;font-weight:700}.mini-btn:hover{background:#991b1b;color:#fff}.muted{color:#9ca3af}.rich-timeline .timeline-item{cursor:pointer;position:relative;border-left:none;border:1px solid #f1d7d7}.time-dot{display:inline-block;background:#7f1d1d;color:#fff;border-radius:999px;padding:3px 9px;font-size:12px;margin-bottom:8px}.legend-row{font-size:12px;color:#6b7280;margin-bottom:10px;display:flex;gap:8px;align-items:center}.legend-dot{width:10px;height:10px;border-radius:50%;display:inline-block}.legend-dot.approve{background:#b91c1c}.legend-dot.disapprove{background:#f59e0b}.bar.two{position:relative}.bar.two span{background:#b91c1c}.bar.two em{display:block;height:100%;background:#f59e0b;border-radius:999px;position:absolute;right:0;top:0;opacity:.75}.chart-row small{width:92px;color:#6b7280}.edge-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px}.edge-card{background:#fff;border:1px solid #f1d7d7;border-radius:18px;padding:14px;cursor:pointer}.edge-title{display:flex;justify-content:space-between;gap:8px;color:#7f1d1d}.rich-detail{display:grid;gap:14px}.detail-section{border:1px solid #f1d7d7;background:#fff;border-radius:16px;padding:14px}.detail-section.emphasis{background:linear-gradient(180deg,#fff8f8,#fff)}.detail-section h3{margin:0 0 8px;color:#7f1d1d}.detail-section p{margin:0;line-height:1.75;color:#374151}.tag-group{margin:9px 0}.tag-group b{display:block;color:#7f1d1d;font-size:12px;margin-bottom:5px}.info-list{display:grid;gap:8px}.info-row{display:grid;grid-template-columns:170px 1fr;gap:10px;border-bottom:1px dashed #f1d7d7;padding-bottom:8px}.info-row b{color:#7f1d1d}.info-row span{line-height:1.65;color:#374151}.source-section{background:#fffdf5}
|
||||
@media (max-width:900px){.rich-cards{grid-template-columns:1fr}.module-summary{justify-content:flex-start}.info-row{grid-template-columns:1fr}.risk-row{grid-template-columns:80px 1fr 32px}}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header"><div class="header-inner"><div class="title"><h1>台湾民进党政要与政治生态知识展示平台</h1><p>三圈人物增强版|A/B/C 159人|关系边表重建|6张关系图原图</p></div><div class="search"><span>🔎</span><input id="globalSearch" placeholder="搜索人物、事件、派系、地区、关系、政策标签……" /></div></div></header>
|
||||
<div class="container">
|
||||
<div id="dashboard" class="dashboard"></div>
|
||||
<div id="tabs" class="tabs"></div>
|
||||
<section id="main" class="panel"></section>
|
||||
</div>
|
||||
<div id="modal" class="modal"><div class="modal-card"><div class="modal-head"><div><h2 id="modalTitle"></h2><div id="modalSub" class="muted"></div></div><button class="close" onclick="closeModal()">×</button></div><div id="modalBody" style="padding:18px 22px"></div></div></div>
|
||||
<script id="app-data" type="application/json">{"meta": {"project_name": "台湾民进党政要与政治生态知识展示平台", "version": "1.1-enhanced-three-circle-network", "language": "zh-CN", "description": "用于展示台湾民进党政要、派系、事件、制度、民调、舆情与认知域传播力量的结构化知识库。", "last_updated": "2026-06-24", "data_status": "prototype_ready", "source_workbook": "dpp_risk_database_with_relationships.xlsx", "notes": "本页面为研究型知识库原型。风险等级表示核验优先级,不代表定罪或违法事实。", "source_workbooks": ["dpp_politicians_overview.xlsx", "dpp_risk_database_with_relationships.xlsx", "dpp_network_maps_package.zip"], "enhancement_note": "已用三圈人物总览表补齐159名人物,并按姓名重建关系表映射,加入6张关系图原图。"}, "global_config": {"default_tab": "people_network", "enable_global_search": true, "enable_filters": true, "enable_modal_detail": true, "enable_secondary_pages": true, "enable_relation_jump": true, "enable_source_trace": true}, "dictionaries": {"entity_types": ["人物", "机构", "政党", "派系", "事件", "法规", "地区", "研究成果", "平台", "舆情主体"], "risk_levels": ["低", "中", "中高", "高", "极高"], "evidence_levels": ["S", "A", "B", "C", "D", "A/S", "A/B"], "parties": ["民进党", "国民党", "民众党", "时代力量", "无党籍", "其他"], "platforms": ["Facebook", "Threads", "PTT", "Dcard", "YouTube", "LINE", "TikTok", "电视政论", "新闻媒体"]}, "dashboard": {"人物数量": 159, "政治事件": 17, "高风险地区": 9, "研究成果": 7, "民调条目": 14, "认知域主体": 10, "第一圈A": 15, "第二圈B": 73, "第三圈C": 71, "关系数量": 60, "关系节点": 73, "关系图原图": 6}, "modules": [{"module_id": "people_network", "module_name": "政要人物与派系网络", "description": "展示民进党三圈人物(A/B/C)、职务经历、所属系统、派系倾向、核心人脉、政策标签,以及规范化关系图谱和6张关系图原图。", "icon": "人物", "datasets": [{"key": "people_full", "name": "三圈人物", "type": "people_circles", "idField": "人物ID", "titleField": "姓名"}, {"key": "people_full", "name": "人物总表", "type": "table", "idField": "人物ID", "titleField": "姓名"}, {"key": "relationships_enhanced", "name": "互动关系图", "type": "network", "idField": "关系ID", "titleField": "关系细类"}, {"key": "relationships_enhanced", "name": "关系边表", "type": "table", "idField": "关系ID", "titleField": "关系细类"}, {"key": "network_maps", "name": "关系图原图", "type": "map_gallery", "idField": "map_id", "titleField": "title"}], "filters": ["层级", "圈层", "地区", "所属系统", "派系倾向", "政策标签", "可信度"], "detail_mode": "modal_or_network_detail"}, {"module_id": "risk_business_judicial_region", "module_name": "政商司法与地区风险", "description": "展示政治献金、标案、企业组织、司法监察、地区风险评分与地方选举风险。", "icon": "风险", "datasets": [{"key": "region_risks", "name": "地区风险", "type": "cards", "idField": "地区ID", "titleField": "县市"}, {"key": "judicial_records", "name": "司法监察", "type": "table", "idField": "记录ID", "titleField": "人物"}, {"key": "donations", "name": "政治献金", "type": "table", "idField": "献金ID", "titleField": "候选人/政党"}, {"key": "bids", "name": "标案资料", "type": "table", "idField": "标案ID", "titleField": "标案名称/关键词"}], "filters": ["地区", "风险等级", "证据等级", "核验状态", "产业类别"], "detail_mode": "modal_or_detail"}, {"module_id": "political_system_laws", "module_name": "台湾政治制度与法规机制", "description": "展示台湾政治制度、政党制度、选举制度、独统议题、政策法规、权力机构和特殊政治机制。", "icon": "制度", "datasets": [{"key": "political_systems", "name": "制度总览", "type": "cards", "idField": "system_id", "titleField": "制度名称"}, {"key": "institutions", "name": "权力机构", "type": "cards", "idField": "institution_id", "titleField": "机构名称"}, {"key": "laws", "name": "政策法规", "type": "table", "idField": "law_id", "titleField": "法规名称"}, {"key": "election_mechanisms", "name": "选举与公投", "type": "table", "idField": "mechanism_id", "titleField": "机制名称"}, {"key": "cross_strait_policies", "name": "独统与两岸", "type": "cards", "idField": "cross_id", "titleField": "议题"}, {"key": "dpp_primary_mechanisms", "name": "党内初选与派系机制", "type": "table", "idField": "dpp_mech_id", "titleField": "机制名称"}, {"key": "special_mechanisms", "name": "特殊政治机制", "type": "cards", "idField": "spm_id", "titleField": "机制名称"}], "filters": ["风险等级", "关联栏目", "适用对象"], "detail_mode": "secondary_page"}, {"module_id": "reseaLine truncated
|
||||
<script>
|
||||
const DATA = JSON.parse(document.getElementById('app-data').textContent);
|
||||
const state = { module: 'people_network', viewKey: null, query: '', filters: {}, relCategory:'全部', relMin:4, relRisk:false, relFocus:'' };
|
||||
const $ = s => document.querySelector(s);
|
||||
const el = (tag, cls='', html='') => { const e=document.createElement(tag); if(cls)e.className=cls; if(html!==undefined)e.innerHTML=html; return e; };
|
||||
function esc(x){ return String(x ?? '').replace(/[&<>'"]/g, m=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[m])); }
|
||||
function isEmpty(v){ return v===undefined || v===null || v==='' || (Array.isArray(v)&&v.length===0); }
|
||||
function flatText(v){
|
||||
if(Array.isArray(v)) return v.map(flatText).filter(Boolean).join('、');
|
||||
if(v && typeof v==='object') return Object.entries(v).filter(([k,val])=>!isEmpty(val)&&k!=='data_uri').map(([k,val])=>`${k}:${flatText(val)}`).join(';');
|
||||
return String(v ?? '');
|
||||
}
|
||||
function arrText(v){ return flatText(v); }
|
||||
function getTitle(item, cfg){ return item?.[cfg?.titleField] || item?.['姓名'] || item?.['事件名'] || item?.['名称'] || item?.['标题'] || item?.['机制名称'] || item?.['主体名称'] || item?.['关系细类'] || item?.['title'] || item?.['name'] || '未命名'; }
|
||||
function currentModule(){ return DATA.modules.find(m=>m.module_id===state.module) || DATA.modules[0]; }
|
||||
function currentDatasetCfg(){ const m=currentModule(); return (m.datasets||[]).find(d=>d.key===state.viewKey) || (m.datasets||[])[0]; }
|
||||
function splitTags(v){
|
||||
if(Array.isArray(v)) return v.filter(Boolean).map(String);
|
||||
if(isEmpty(v)) return [];
|
||||
return String(v).split(/[、,,;;\/|]/).map(s=>s.trim()).filter(Boolean).slice(0,12);
|
||||
}
|
||||
function tagHtml(tags, cls='') { return splitTags(tags).map(t=>`<span class="tag ${cls}">${esc(t)}</span>`).join(''); }
|
||||
function val(item, keys, fallback=''){
|
||||
for(const k of keys){ if(!isEmpty(item[k])) return item[k]; }
|
||||
return fallback;
|
||||
}
|
||||
function init(){ renderDashboard(); renderTabs(); bindSearch(); renderModule(); }
|
||||
function renderDashboard(){
|
||||
const d=DATA.dashboard||{}; const keys=['人物数量','第一圈A','第二圈B','第三圈C','关系数量','关系节点','关系图原图'];
|
||||
$('#dashboard').innerHTML = keys.map(k=>`<div class="stat"><div class="k">${esc(k)}</div><div class="v">${esc(d[k]??0)}</div></div>`).join('');
|
||||
}
|
||||
function renderTabs(){
|
||||
$('#tabs').innerHTML = DATA.modules.map(m=>`<button class="tab ${m.module_id===state.module?'active':''}" data-id="${m.module_id}">${esc(m.module_name)}</button>`).join('');
|
||||
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{state.module=b.dataset.id; state.viewKey=null; state.filters={}; renderTabs(); renderModule();});
|
||||
}
|
||||
function bindSearch(){ $('#globalSearch').oninput = e=>{state.query=e.target.value.trim(); renderModuleBody();}; }
|
||||
function renderModule(){
|
||||
const m=currentModule(); if(!state.viewKey) state.viewKey=(m.datasets&&m.datasets[0]&&m.datasets[0].key)||'';
|
||||
if(m.module_id==='people_network'){
|
||||
$('#main').innerHTML = `<div class="module-head"><div><h2>${esc(m.module_name)}</h2><p>${esc(m.description||'')}</p></div><div class="notice">风险线与关系线仅表示研究型关联、职务/派系/事件/议题连接,不等于违法认定。关系表已按姓名重建三圈人物ID,并保留原始ID作为溯源字段。</div></div><div id="subtabs" class="subtabs"></div><div id="toolbar" class="toolbar"></div><div id="content" class="content"></div>`;
|
||||
} else {
|
||||
$('#main').innerHTML = `<div class="module-head compact-head"><div><h2>${esc(m.module_name)}</h2></div><div id="moduleSummary" class="module-summary"></div></div><div id="subtabs" class="subtabs"></div><div id="toolbar" class="toolbar"></div><div id="content" class="content"></div>`;
|
||||
}
|
||||
renderSubtabs(); renderModuleBody();
|
||||
}
|
||||
function renderSubtabs(){
|
||||
const m=currentModule(); $('#subtabs').innerHTML=(m.datasets||[]).map(d=>`<button class="subtab ${d.key===state.viewKey?'active':''}" data-key="${d.key}">${esc(d.name)}</button>`).join('');
|
||||
document.querySelectorAll('.subtab').forEach(b=>b.onclick=()=>{state.viewKey=b.dataset.key; state.filters={}; renderSubtabs(); renderModuleBody();});
|
||||
}
|
||||
function renderModuleSummary(raw, cfg){
|
||||
const box=$('#moduleSummary'); if(!box) return;
|
||||
const m=currentModule();
|
||||
if(m.module_id==='people_network') return;
|
||||
const fields = {
|
||||
risk_business_judicial_region: ['region_risks','judicial_records','donations','bids'],
|
||||
political_system_laws: ['political_systems','institutions','laws','special_mechanisms'],
|
||||
research_outputs: ['research_outputs'],
|
||||
events_opinion_polling: ['events','poll_time_series','platform_metrics','impact_assessments'],
|
||||
cognitive_public_opinion_forces: ['cognitive_actors','party_comm_units','gov_info_units','fact_check_network']
|
||||
}[m.module_id] || [];
|
||||
box.innerHTML = fields.map(k=>`<span class="mini-stat"><b>${esc((DATA.datasets[k]||[]).length)}</b>${esc(datasetLabel(k))}</span>`).join('');
|
||||
}
|
||||
function datasetLabel(k){ return ({region_risks:'地区',judicial_records:'司法',donations:'献金',bids:'标案',political_systems:'制度',institutions:'机构',laws:'法规',special_mechanisms:'特殊机制',research_outputs:'研究',events:'事件',poll_time_series:'民调',platform_metrics:'平台',impact_assessments:'影响',cognitive_actors:'主体',party_comm_units:'传播部门',gov_info_units:'信息机构',fact_check_network:'查核组织'}[k]||k); }
|
||||
function renderToolbar(data, cfg){
|
||||
const m=currentModule(); const fields = m.filters || [];
|
||||
let html = '';
|
||||
if(cfg.type==='network' && (cfg.key==='relationships_enhanced' || state.module==='people_network')){
|
||||
const cats=[...new Set((DATA.datasets.relationships_enhanced||[]).map(r=>r['关系大类']).filter(Boolean))];
|
||||
html += `<select id="relCategory"><option>全部</option>${cats.map(c=>`<option ${state.relCategory===c?'selected':''}>${esc(c)}</option>`).join('')}</select>`;
|
||||
html += `<select id="relMin"><option value="1">强度≥1</option><option value="3" ${state.relMin==3?'selected':''}>强度≥3</option><option value="4" ${state.relMin==4?'selected':''}>强度≥4</option><option value="5" ${state.relMin==5?'selected':''}>强度=5</option></select>`;
|
||||
html += `<label style="display:flex;gap:6px;align-items:center"><input id="relRisk" type="checkbox" ${state.relRisk?'checked':''}>只看高风险</label>`;
|
||||
html += `<input id="relFocus" placeholder="聚焦人物/节点,如 赖清德、台南" value="${esc(state.relFocus)}" />`;
|
||||
html += `<span class="hint">互动图默认显示强度≥4,可调低强度查看更多关系。</span>`;
|
||||
$('#toolbar').innerHTML=html;
|
||||
$('#relCategory').onchange=e=>{state.relCategory=e.target.value; renderModuleBody();};
|
||||
$('#relMin').onchange=e=>{state.relMin=+e.target.value; renderModuleBody();};
|
||||
$('#relRisk').onchange=e=>{state.relRisk=e.target.checked; renderModuleBody();};
|
||||
$('#relFocus').oninput=e=>{state.relFocus=e.target.value.trim(); renderModuleBody(false);};
|
||||
return;
|
||||
}
|
||||
fields.forEach(f=>{
|
||||
const vals=[...new Set(data.map(x=>x[f]).filter(v=>v!==undefined&&v!==null&&v!==''))].slice(0,200);
|
||||
if(vals.length>1 && vals.length<80) html += `<select data-filter="${esc(f)}"><option value="">${esc(f)}:全部</option>${vals.map(v=>`<option value="${esc(v)}" ${state.filters[f]===String(v)?'selected':''}>${esc(v)}</option>`).join('')}</select>`;
|
||||
});
|
||||
html += `<span class="hint">当前:${esc(data.length)} 条;搜索与筛选在当前子页内生效。</span>`;
|
||||
$('#toolbar').innerHTML=html;
|
||||
document.querySelectorAll('[data-filter]').forEach(s=>s.onchange=e=>{state.filters[e.target.dataset.filter]=e.target.value; renderModuleBody();});
|
||||
}
|
||||
function filterData(data){
|
||||
const q=state.query.toLowerCase();
|
||||
return data.filter(item=>{
|
||||
const passFilters=Object.entries(state.filters).every(([k,v])=>!v || String(item[k]??'')===v);
|
||||
if(!passFilters) return false;
|
||||
if(!q) return true;
|
||||
return flatText(item).toLowerCase().includes(q);
|
||||
});
|
||||
}
|
||||
function renderModuleBody(updateToolbar=true){
|
||||
const cfg=currentDatasetCfg(); const raw=DATA.datasets[cfg.key] || [];
|
||||
renderModuleSummary(raw,cfg);
|
||||
if(updateToolbar) renderToolbar(raw, cfg);
|
||||
let data=filterData(raw);
|
||||
if(cfg.type==='people_circles') return renderPeopleCircles(data);
|
||||
if(cfg.type==='network'){
|
||||
if(cfg.key==='event_entity_edges') return renderEventEntityNetwork(data,cfg);
|
||||
return renderNetwork();
|
||||
}
|
||||
if(cfg.type==='map_gallery') return renderMapGallery(data);
|
||||
if(cfg.type==='cards') return renderCards(data,cfg);
|
||||
if(cfg.type==='timeline') return renderTimeline(data,cfg);
|
||||
if(cfg.type==='chart_table') return renderPollChart(data,cfg);
|
||||
return renderTable(data,cfg);
|
||||
}
|
||||
function levelClass(l){return l==='A'?'levelA':(l==='B'?'levelB':'levelC')}
|
||||
function renderPeopleCircles(data){
|
||||
const groups={A:[],B:[],C:[]}; data.forEach(p=>{ if(groups[p['层级']]) groups[p['层级']].push(p); });
|
||||
const desc={A:'总统府、行政院、党主席、地方执政与国会核心。',B:'中常委、地方首长、立委、党务/传播/国安关键节点。',C:'地方立委、专业不分区、党务节点、历史人物与风险案例。'};
|
||||
$('#content').innerHTML = `<div class="circle-grid">${['A','B','C'].map(level=>`<div class="circle-col"><h3>${esc(DATA.datasets.people_circles.find(x=>x.level===level)?.label||level)}<span class="tag ${levelClass(level)}">${groups[level].length}人</span></h3><div class="desc">${desc[level]}</div>${groups[level].map(personCard).join('')}</div>`).join('')}</div>`;
|
||||
document.querySelectorAll('[data-person]').forEach(x=>x.onclick=()=>openDetail(DATA.datasets.people_full.find(p=>p['人物ID']===x.dataset.person), '人物详情'));
|
||||
}
|
||||
function personCard(p){
|
||||
const tags=[p['地区'], p['所属系统'], p['派系倾向']].filter(Boolean).slice(0,3).map(t=>`<span class="tag">${esc(t)}</span>`).join('');
|
||||
const rel=p['关系数量']?`<span class="tag blue">关系${p['关系数量']}</span>`:'';
|
||||
const risk=p['高风险关系数量']?`<span class="tag risk">高风险${p['高风险关系数量']}</span>`:'';
|
||||
return `<div class="person-card" data-person="${esc(p['人物ID'])}"><div class="name"><span>${esc(p['姓名'])}</span><span class="tag ${levelClass(p['层级'])}">${esc(p['层级'])}</span></div><div class="role">${esc(p['当前身份'])}</div><div class="tags">${tags}${rel}${risk}</div></div>`;
|
||||
}
|
||||
function renderCards(data,cfg){
|
||||
if(state.module==='people_network'){
|
||||
$('#content').innerHTML = data.length?`<div class="cards">${data.map(it=>`<div class="card" data-id="${esc(it[cfg.idField]||'')}"><h3>${esc(getTitle(it,cfg))}</h3><p>${esc(Object.entries(it).filter(([k,v])=>v&&k!==cfg.titleField&&k!==cfg.idField).slice(0,5).map(([k,v])=>`${k}:${arrText(v)}`).join('\n'))}</p></div>`).join('')}</div>`:`<div class="empty">没有匹配数据</div>`;
|
||||
document.querySelectorAll('.card').forEach((x,i)=>x.onclick=()=>openDetail(data[i], getTitle(data[i],cfg)));
|
||||
return;
|
||||
}
|
||||
$('#content').innerHTML = data.length?`<div class="cards rich-cards">${data.map((it,i)=>richCard(it,cfg,i)).join('')}</div>`:`<div class="empty">没有匹配数据</div>`;
|
||||
document.querySelectorAll('.rich-card').forEach(x=>x.onclick=()=>openDetail(data[+x.dataset.i], getTitle(data[+x.dataset.i],cfg)));
|
||||
}
|
||||
function richCard(it,cfg,i){
|
||||
const key=cfg.key, title=getTitle(it,cfg);
|
||||
if(key==='region_risks') return regionCard(it,i);
|
||||
if(key==='political_systems'||key==='institutions'||key==='cross_strait_policies'||key==='special_mechanisms') return systemCard(it,cfg,i);
|
||||
if(key==='research_outputs') return researchCard(it,i);
|
||||
if(key==='person_opinion_profiles'||key==='impact_assessments') return opinionCard(it,cfg,i);
|
||||
if(key==='cognitive_actors'||key==='gov_info_units'||key==='fact_check_network'||key==='platform_mechanisms') return actorCard(it,cfg,i);
|
||||
return genericCard(it,cfg,i);
|
||||
}
|
||||
function genericCard(it,cfg,i){
|
||||
const title=getTitle(it,cfg); const subtitle=val(it,['类型','阵营','机构','地区','功能定位','当前状态','年份','风险等级'],'');
|
||||
const tags=[...splitTags(val(it,['主题标签','主要平台','核心功能','主要风险类型','关联栏目','平台/渠道','内容特征'],''))].slice(0,6);
|
||||
const desc=val(it,['核心内容','说明','风险摘要','主要职能','核心结论','政治影响','典型任务','认知域相关职责','短期影响'],'');
|
||||
return `<article class="card rich-card" data-i="${i}"><div class="card-top"><h3>${esc(title)}</h3>${subtitle?`<span class="pill">${esc(subtitle)}</span>`:''}</div>${tags.length?`<div class="tags">${tagHtml(tags)}</div>`:''}${desc?`<p class="summary">${esc(flatText(desc))}</p>`:''}${miniFields(it,['证据等级','可信度','核验状态','风险等级','数据用途','适用对象'])}</article>`;
|
||||
}
|
||||
function regionCard(it,i){
|
||||
const score=Number(it['综合分']||0); const risk=it['风险等级']||'';
|
||||
const people=splitTags(it['核心人物']).slice(0,8);
|
||||
const risks=splitTags(it['主要风险类型']).slice(0,8);
|
||||
const bars=['司法风险(30%)','资金风险(20%)','土地/产业风险(20%)','派系/地方风险(15%)','舆情风险(10%)'].map(k=>riskBar(k,it[k])).join('');
|
||||
return `<article class="card rich-card region-card" data-i="${i}"><div class="card-top"><h3>${esc(it['县市'])}</h3><span class="pill risk-pill">${esc(risk)}|${esc(it['综合分'])}</span></div><div class="meta-line">${esc(it['执政状态/政治属性']||'')}</div><div class="tags">${people.map(x=>`<span class="tag blue">${esc(x)}</span>`).join('')}${risks.map(x=>`<span class="tag risk">${esc(x)}</span>`).join('')}</div><div class="risk-bars">${bars}</div><p class="summary"><b>2026影响:</b>${esc(it['2026选举影响']||'待补充')}</p></article>`;
|
||||
}
|
||||
function riskBar(label,value){ const n=Math.max(0,Math.min(5,Number(value)||0)); return `<div class="risk-row"><span>${esc(label.replace(/\(.+?\)/,''))}</span><div class="riskbar"><i style="width:${n*20}%"></i></div><b>${n}/5</b></div>`; }
|
||||
function systemCard(it,cfg,i){
|
||||
const title=getTitle(it,cfg); const desc=val(it,['核心内容','主要职能','核心规则','触发条件','政治含义'],'');
|
||||
const tags=val(it,['标签','关联栏目','适用对象','风险等级'],'');
|
||||
return `<article class="card rich-card system-card" data-i="${i}"><div class="card-top"><h3>${esc(title)}</h3>${it['风险等级']?`<span class="pill risk-pill">${esc(it['风险等级'])}</span>`:''}</div>${tags?`<div class="tags">${tagHtml(tags)}</div>`:''}<p class="summary">${esc(flatText(desc))}</p>${miniFields(it,['机构定位','主管/适用','程序门槛','数据用途','来源'])}</article>`;
|
||||
}
|
||||
function researchCard(it,i){
|
||||
return `<article class="card rich-card research-card" data-i="${i}"><div class="card-top"><h3>${esc(it['标题'])}</h3><span class="pill">${esc(it['可信度']||'')}</span></div><div class="meta-line">${esc(it['机构']||'')}|${esc(it['年份']||'')}|${esc(it['类型']||'')}</div><div class="tags">${tagHtml(it['主题标签'])}</div><div class="bullet-box">${toBullets(it['核心结论'])}</div>${miniFields(it,['可下载性','适用栏目'])}</article>`;
|
||||
}
|
||||
function opinionCard(it,cfg,i){
|
||||
const title=getTitle(it,cfg); const risk=it['舆情风险']||it['风险评分']||'';
|
||||
const pos=it['正面形象'], neg=it['负面标签'];
|
||||
return `<article class="card rich-card opinion-card" data-i="${i}"><div class="card-top"><h3>${esc(title)}</h3>${risk?`<span class="pill risk-pill">风险 ${esc(risk)}</span>`:''}</div>${pos?`<div class="label-row"><b>正面形象</b><div class="tags">${tagHtml(pos,'blue')}</div></div>`:''}${neg?`<div class="label-row"><b>负面标签</b><div class="tags">${tagHtml(neg,'risk')}</div></div>`:''}<p class="summary">${esc(flatText(val(it,['短期影响','长期影响','平台敏感性','观察指标'],'')))}</p></article>`;
|
||||
}
|
||||
function actorCard(it,cfg,i){
|
||||
const title=getTitle(it,cfg);
|
||||
return `<article class="card rich-card actor-card" data-i="${i}"><div class="card-top"><h3>${esc(title)}</h3><span class="pill">${esc(val(it,['类型','功能定位','政党/阵营'],'主体'))}</span></div>${val(it,['阵营','政党'])?`<div class="meta-line">${esc(val(it,['阵营','政党']))}</div>`:''}<div class="tags">${tagHtml(val(it,['核心功能','主要平台','平台/渠道','平台'],''))}</div><p class="summary">${esc(flatText(val(it,['认知域相关职责','主要功能','政治传播作用','内容特征','争议风险'],'')))}</p>${miniFields(it,['典型场景','适合关联事件','数据可得性','关键指标','关联栏目'])}</article>`;
|
||||
}
|
||||
function miniFields(it, keys){ return `<div class="mini-fields">${keys.filter(k=>!isEmpty(it[k])).map(k=>`<div><span>${esc(k)}</span><b>${esc(flatText(it[k]))}</b></div>`).join('')}</div>`; }
|
||||
function toBullets(v){ const arr=Array.isArray(v)?v:splitTags(v); if(!arr.length) return ''; return `<ul>${arr.slice(0,5).map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`; }
|
||||
function renderTable(data,cfg){
|
||||
if(!data.length) return $('#content').innerHTML='<div class="empty">没有匹配数据</div>';
|
||||
const keyMap={
|
||||
judicial_records:['记录ID','人物','机关','类型','阶段','主要争点','结果/状态','证据等级','下一步跟踪'],
|
||||
donations:['献金ID','年度/选举','候选人/政党','捐款人/企业','捐款人类型','产业类别','核验状态','风险信号'],
|
||||
bids:['标案ID','标案名称/关键词','机关','地区','产业类别','得标厂商','关联人物','风险信号'],
|
||||
laws:['law_id','法规名称','主管/适用','核心条款摘要','关联栏目','详情展示'],
|
||||
election_mechanisms:['mechanism_id','机制名称','核心规则','适用对象','数据用途'],
|
||||
dpp_primary_mechanisms:['dpp_mech_id','机制名称','核心内容','数据用途'],
|
||||
narrative_frames:['frame_id','event_id','行动方','话术类型','核心话术','对抗话术'],
|
||||
platform_metrics:['metric_id','event_id','平台范围','量化数据','说明','可信度'],
|
||||
party_comm_units:['unit_id','政党','部门/机制','公开可见功能','典型任务','关联人物'],
|
||||
party_media_matrix:['matrix_id','政党/阵营','主要平台','传播形态','主要受众','内容特征','观察指标'],
|
||||
cognitive_risks:['risk_id','风险类型','相关主体','表现','评估指标','备注']
|
||||
};
|
||||
const priority=['人物ID','姓名','层级','圈层','当前身份','所属系统','派系倾向','地区','关系ID','Source_名称','Target_名称','关系大类','关系细类','关系强度(1-5)','是否高风险','证据等级','简要说明'];
|
||||
let keys=keyMap[cfg.key] || [...new Set([...priority.filter(k=>k in data[0]), ...Object.keys(data[0])])].filter(k=>k!=='data_uri').slice(0,10);
|
||||
$('#content').innerHTML = `<div class="table-wrap rich-table"><table><thead><tr>${keys.map(k=>`<th>${esc(k)}</th>`).join('')}<th>详情</th></tr></thead><tbody>${data.map((row,idx)=>`<tr data-row="${idx}">${keys.map(k=>`<td>${cellHtml(row[k],k)}</td>`).join('')}<td><button class="mini-btn" data-detail="${idx}">查看</button></td></tr>`).join('')}</tbody></table></div>`;
|
||||
document.querySelectorAll('[data-detail]').forEach(btn=>btn.onclick=(e)=>{e.stopPropagation(); openDetail(data[+btn.dataset.detail], getTitle(data[+btn.dataset.detail],cfg));});
|
||||
document.querySelectorAll('tbody tr').forEach(tr=>tr.onclick=()=>openDetail(data[+tr.dataset.row], getTitle(data[+tr.dataset.row],cfg)));
|
||||
}
|
||||
function cellHtml(v,k){
|
||||
if(isEmpty(v)) return '<span class="muted">—</span>';
|
||||
if(Array.isArray(v)) return `<div class="tags inline-tags">${tagHtml(v)}</div>`;
|
||||
const s=String(v);
|
||||
if(['证据等级','可信度','风险等级','核验状态','是否高风险'].some(x=>k.includes(x))) return `<span class="tag ${s.includes('高')||s==='是'?'risk':'blue'}">${esc(s)}</span>`;
|
||||
return esc(s.length>95?s.slice(0,95)+'…':s);
|
||||
}
|
||||
function renderTimeline(data,cfg){
|
||||
$('#content').innerHTML = data.length?`<div class="timeline rich-timeline">${data.map((it,i)=>timelineItem(it,cfg,i)).join('')}</div>`:`<div class="empty">没有匹配数据</div>`;
|
||||
document.querySelectorAll('.timeline-item').forEach(x=>x.onclick=()=>openDetail(data[+x.dataset.i], getTitle(data[+x.dataset.i],cfg)));
|
||||
}
|
||||
function timelineItem(it,cfg,i){
|
||||
const title=getTitle(it,cfg); const time=val(it,['起始/关键日期','时间','publish_date','start_date'],'');
|
||||
const meta=[val(it,['类型','攻防主题','地区'],''), val(it,['当前状态','平台','司法/监察阶段'],'')].filter(Boolean).join('|');
|
||||
const summary=val(it,['风险摘要','主要框架','核心争议','说明','2026选举影响'],'');
|
||||
return `<div class="timeline-item" data-i="${i}"><div class="time-dot">${esc(time||'时间待补')}</div><b>${esc(title)}</b>${meta?`<div class="meta-line">${esc(meta)}</div>`:''}<p>${esc(flatText(summary))}</p>${tagHtml(val(it,['涉及人物','参与主体','舆情标签','平台'],''))}</div>`;
|
||||
}
|
||||
function renderPollChart(data,cfg){
|
||||
const series=data.filter(x=>x.approve_value!==undefined && x.approve_value!=='' && !isNaN(+x.approve_value)); const max=Math.max(1,...series.map(x=>Math.max(+x.approve_value,+x.disapprove_value||0)));
|
||||
const rows=series.slice(0,80).map((x,i)=>`<div class="chart-row" data-i="${i}"><div class="chart-label">${esc(x.publish_date||x.source)}<br>${esc(x.target||'')}</div><div class="bar two"><span style="width:${(+x.approve_value/max)*100}%"></span><em style="width:${((+x.disapprove_value||0)/max)*100}%"></em></div><b>${esc(x.approve_value)}%</b><small>不满意 ${esc(x.disapprove_value??'—')}%</small></div>`).join('');
|
||||
const table=data.length?pollTable(data):'';
|
||||
$('#content').innerHTML = `<div class="chart rich-chart"><h3 style="margin-top:0;color:#7f1d1d">民调趋势</h3><div class="legend-row"><span class="legend-dot approve"></span>满意/支持 <span class="legend-dot disapprove"></span>不满意/不支持</div>${rows||'<div class="empty">暂无可绘制数值</div>'}</div>${table}`;
|
||||
document.querySelectorAll('.chart-row').forEach(x=>x.onclick=()=>openDetail(series[+x.dataset.i], '民调详情'));
|
||||
document.querySelectorAll('[data-poll]').forEach(btn=>btn.onclick=(e)=>{e.stopPropagation();openDetail(data[+btn.dataset.poll],'民调详情');});
|
||||
}
|
||||
function pollTable(data){ const keys=['poll_id','source','publish_date','indicator_type','target','approve_value','disapprove_value','neutral_value','related_event_id','method','credibility_level']; return `<div class="table-wrap rich-table" style="margin-top:16px"><table><thead><tr>${keys.map(k=>`<th>${esc(k)}</th>`).join('')}<th>详情</th></tr></thead><tbody>${data.map((r,i)=>`<tr>${keys.map(k=>`<td>${cellHtml(r[k],k)}</td>`).join('')}<td><button class="mini-btn" data-poll="${i}">查看</button></td></tr>`).join('')}</tbody></table></div>`; }
|
||||
function renderEventEntityNetwork(data,cfg){
|
||||
if(!data.length) return $('#content').innerHTML='<div class="empty">没有匹配数据</div>';
|
||||
$('#content').innerHTML=`<div class="edge-card-grid">${data.map((e,i)=>`<div class="edge-card" data-i="${i}"><div class="edge-title"><b>${esc(e.source_entity_name)} → ${esc(e.target_entity_name)}</b><span class="tag blue">强度${esc(e.relation_strength)}</span></div><div class="tags"><span class="tag">${esc(e.source_entity_type)}</span><span class="tag">${esc(e.target_entity_type)}</span><span class="tag risk">${esc(e.relation_type)}</span></div><p>${esc(e.relation_description||'')}</p><p class="summary"><b>风险影响:</b>${esc(e.risk_effect||'')}</p></div>`).join('')}</div>`;
|
||||
document.querySelectorAll('.edge-card').forEach(x=>x.onclick=()=>openDetail(data[+x.dataset.i],'联动关系详情'));
|
||||
}
|
||||
function renderNetwork(){
|
||||
const rels0=DATA.datasets.relationships_enhanced||[]; let rels=rels0.filter(r=>r['关系强度数值']>=state.relMin);
|
||||
if(state.relCategory && state.relCategory!=='全部') rels=rels.filter(r=>r['关系大类']===state.relCategory);
|
||||
if(state.relRisk) rels=rels.filter(r=>r['是否高风险布尔']);
|
||||
const focus=(state.relFocus||state.query||'').toLowerCase(); if(focus) rels=rels.filter(r=>flatText(r).toLowerCase().includes(focus));
|
||||
const nodesMap=new Map(); const allNodes=new Map((DATA.datasets.relationship_nodes||[]).map(n=>[n.node_id,n]));
|
||||
rels.forEach(r=>{ [r.source_norm_id,r.target_norm_id].forEach(id=>{ if(allNodes.has(id)) nodesMap.set(id, allNodes.get(id)); }); });
|
||||
const nodes=[...nodesMap.values()];
|
||||
const svg=buildNetworkSVG(nodes, rels);
|
||||
$('#content').innerHTML=`<div class="network-layout"><div class="network-box">${svg}</div><div class="rel-list"><h3 style="margin-top:0;color:#7f1d1d">关系边表摘要</h3><p style="color:#6b7280;font-size:12px">当前显示 ${rels.length} 条关系、${nodes.length} 个节点。点击节点可聚焦。</p>${rels.map((r,i)=>`<div class="rel-item" data-ri="${i}"><b>${esc(r.Source_名称)} → ${esc(r.Target_名称)}</b><div class="tags"><span class="tag">${esc(r['关系大类'])}</span><span class="tag blue">强度${esc(r['关系强度(1-5)'])}</span>${r['是否高风险']==='是'?'<span class="tag risk">高风险</span>':''}</div><p>${esc(r['关系细类']||'')}:${esc(r['简要说明']||'')}</p></div>`).join('')}</div></div>`;
|
||||
document.querySelectorAll('.rel-item').forEach(x=>x.onclick=()=>openDetail(rels[+x.dataset.ri], '关系详情'));
|
||||
document.querySelectorAll('.node').forEach(n=>n.onclick=()=>{state.relFocus=n.dataset.name; renderModuleBody(false);});
|
||||
}
|
||||
function nodeColor(n){ if(n.type.includes('地区')) return '#f59e0b'; if(n.type.includes('组织')||n.type.includes('机构')) return '#2563eb'; if(n.type.includes('事件')||n.type.includes('议题')) return '#dc2626'; if(n.type.includes('派系')) return '#7e22ce'; if(n.level==='A') return '#065f46'; if(n.level==='B') return '#10b981'; if(n.level==='C') return '#86efac'; return '#9ca3af'; }
|
||||
function buildNetworkSVG(nodes, rels){
|
||||
const w=1200,h=680,cx=w/2,cy=h/2; const rings={A:120,B:210,C:300,other:370}; const by={A:[],B:[],C:[],other:[]};
|
||||
nodes.forEach(n=>{ if(n.level==='A')by.A.push(n); else if(n.level==='B')by.B.push(n); else if(n.level==='C')by.C.push(n); else by.other.push(n); });
|
||||
const pos={}; Object.entries(by).forEach(([key,arr],ri)=>{ const r=rings[key]; const offset=(ri*.31); arr.forEach((n,i)=>{ const a=(Math.PI*2*i/Math.max(1,arr.length))+offset; pos[n.node_id]={x:cx+r*Math.cos(a),y:cy+r*Math.sin(a)}; }); });
|
||||
const edgeEls=rels.map(r=>{ const a=pos[r.source_norm_id], b=pos[r.target_norm_id]; if(!a||!b)return ''; const cls=`edge ${r['是否高风险']==='是'?'high':''} ${r['关系强度数值']>=4?'strong':'weak'}`; return `<line class="${cls}" x1="${a.x.toFixed(1)}" y1="${a.y.toFixed(1)}" x2="${b.x.toFixed(1)}" y2="${b.y.toFixed(1)}"/>`; }).join('');
|
||||
const nodeEls=nodes.map(n=>{ const p=pos[n.node_id]; if(!p)return ''; const color=nodeColor(n), rad=n.level==='A'?12:(n.level==='B'?9:(n.level==='C'?7:8)); const label=n.name.length>7?n.name.slice(0,7)+'…':n.name; return `<g class="node" data-name="${esc(n.name)}"><circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="${rad}" fill="${color}" stroke="#fff" stroke-width="2"/><text x="${(p.x+rad+3).toFixed(1)}" y="${(p.y+4).toFixed(1)}">${esc(label)}</text><title>${esc(n.name+'|'+n.type+'|'+(n.circle||''))}</title></g>`; }).join('');
|
||||
const legend=`<g transform="translate(24,22)"><rect width="245" height="112" rx="14" fill="rgba(255,255,255,.88)" stroke="#f1d5d5"/><text x="14" y="24" font-size="13" font-weight="700" fill="#7f1d1d">节点图例</text><circle cx="18" cy="46" r="7" fill="#065f46"/><text x="32" y="50" font-size="11">A核心</text><circle cx="92" cy="46" r="7" fill="#10b981"/><text x="106" y="50" font-size="11">B关键</text><circle cx="168" cy="46" r="7" fill="#86efac"/><text x="182" y="50" font-size="11">C外围</text><circle cx="18" cy="73" r="7" fill="#2563eb"/><text x="32" y="77" font-size="11">机构</text><circle cx="92" cy="73" r="7" fill="#dc2626"/><text x="106" y="77" font-size="11">事件/风险</text><circle cx="18" cy="100" r="7" fill="#7e22ce"/><text x="32" y="104" font-size="11">派系/系统</text></g>`;
|
||||
return `<svg class="network-svg" viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg">${legend}<g>${edgeEls}</g><g>${nodeEls}</g></svg>`;
|
||||
}
|
||||
function renderMapGallery(data){
|
||||
const maps=data||[]; if(!maps.length)return $('#content').innerHTML='<div class="empty">没有关系图原图</div>';
|
||||
const active=maps[0];
|
||||
$('#content').innerHTML=`<div class="gallery"><div><div class="map-list">${maps.map((m,i)=>`<div class="map-thumb ${i===0?'active':''}" data-mi="${i}"><b>${esc(m.title)}</b><p style="font-size:12px;color:#6b7280">${esc(m.description)}</p><img src="${m.data_uri}"></div>`).join('')}</div><div class="legend"><b>图例说明</b><br>${esc((DATA.datasets.network_map_legend&&DATA.datasets.network_map_legend[0]&&DATA.datasets.network_map_legend[0].readme)||'')}</div></div><div class="map-view"><h3 id="mapTitle" style="margin-top:0;color:#7f1d1d">${esc(active.title)}</h3><p id="mapDesc" style="color:#6b7280">${esc(active.description)}</p><img id="mapImg" src="${active.data_uri}"></div></div>`;
|
||||
document.querySelectorAll('.map-thumb').forEach(x=>x.onclick=()=>{document.querySelectorAll('.map-thumb').forEach(y=>y.classList.remove('active'));x.classList.add('active');const m=maps[+x.dataset.mi];$('#mapTitle').textContent=m.title;$('#mapDesc').textContent=m.description;$('#mapImg').src=m.data_uri;});
|
||||
}
|
||||
function openDetail(obj,title='详情'){
|
||||
if(!obj)return; $('#modalTitle').textContent=title || getTitle(obj,{});
|
||||
if(state.module==='people_network'){
|
||||
$('#modalSub').textContent='点击右上角关闭;字段名称保持原始中文。';
|
||||
$('#modalBody').innerHTML=`<div class="detail-grid">${Object.entries(obj).filter(([k,v])=>v!==''&&v!==null&&v!==undefined&&k!=='data_uri').map(([k,v])=>`<div class="k">${esc(k)}</div><div class="v">${esc(arrText(v))}</div>`).join('')}</div>`;
|
||||
} else {
|
||||
$('#modalSub').textContent='结构化详情;已将原始字段转为页面语言展示。';
|
||||
$('#modalBody').innerHTML=richDetail(obj,title);
|
||||
}
|
||||
$('#modal').classList.add('show');
|
||||
}
|
||||
function richDetail(obj,title){
|
||||
const important=['风险摘要','核心内容','主要争点','核心条款摘要','政治影响','2026选举影响','短期影响','长期影响','说明','核心规则','触发条件','程序门槛','核心话术','对抗话术','认知域相关职责','争议风险','主要功能','风险信号'];
|
||||
const summary=important.filter(k=>!isEmpty(obj[k])).map(k=>`<section class="detail-section emphasis"><h3>${esc(k)}</h3><p>${esc(flatText(obj[k]))}</p></section>`).join('');
|
||||
const tags=Object.entries(obj).filter(([k,v])=>/(标签|平台|人物|地区|阵营|类型|等级|状态|可信度|风险)/.test(k)&&!important.includes(k)&&!isEmpty(v)).slice(0,12).map(([k,v])=>`<div class="tag-group"><b>${esc(k)}</b><div class="tags">${tagHtml(v)}</div></div>`).join('');
|
||||
const source=Object.entries(obj).filter(([k,v])=>/(来源|URL|链接|证据|核验|更新|备注|ID)/.test(k)&&!isEmpty(v)&&k!=='data_uri').map(([k,v])=>`<div><span>${esc(k)}</span><b>${esc(flatText(v))}</b></div>`).join('');
|
||||
const rest=Object.entries(obj).filter(([k,v])=>!isEmpty(v)&&k!=='data_uri'&&!important.includes(k)&&!/(标签|平台|人物|地区|阵营|类型|等级|状态|可信度|风险|来源|URL|链接|证据|核验|更新|备注|ID)/.test(k)).map(([k,v])=>`<div class="info-row"><b>${esc(k)}</b><span>${esc(flatText(v))}</span></div>`).join('');
|
||||
return `<div class="rich-detail">${summary||`<section class="detail-section emphasis"><h3>概要</h3><p>${esc(flatText(obj).slice(0,800))}</p></section>`}${tags?`<section class="detail-section"><h3>标签与关联</h3>${tags}</section>`:''}${rest?`<section class="detail-section"><h3>详细字段</h3><div class="info-list">${rest}</div></section>`:''}${source?`<section class="detail-section source-section"><h3>来源与核验</h3><div class="mini-fields">${source}</div></section>`:''}</div>`;
|
||||
}
|
||||
function closeModal(){ $('#modal').classList.remove('show'); }
|
||||
$('#modal').addEventListener('click',e=>{if(e.target.id==='modal')closeModal();});
|
||||
init();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user