feat(定时任务): 新增第五个 HTML 模板 no.5(只展示)
- no.5.html(174KB)复制进 templates/no5/;数据用 `let DATA = {…}`(let+空格,非
const DATA= 行),当前引擎行注入正则不匹配,故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.5
- 测试覆盖 no.5 存在/只展示/文件可读
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6a6195f883
commit
c0af23aefe
2 files changed
+109
No files matched your search
+9
@@ -157,6 +157,15 @@ TEMPLATES: tuple[TemplateSpec, ...] = (
|
||||
# 当前引擎只支持 const VAR= 行注入,故暂注册为只展示;接转换需扩展元素注入。
|
||||
convertible=False,
|
||||
),
|
||||
TemplateSpec(
|
||||
id="no5",
|
||||
name="no.5",
|
||||
description="",
|
||||
file="no5/no5.html",
|
||||
# 数据用 `let DATA = {…}`(let + 空格,非 const DATA= 行),当前引擎不匹配,
|
||||
# 故暂注册为只展示;接转换需扩展行注入正则。
|
||||
convertible=False,
|
||||
),
|
||||
)
|
||||
|
||||
_TEMPLATES_BY_ID: dict[str, TemplateSpec] = {t.id: t for t in TEMPLATES}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>台湾公务员群体知识数据展示系统 v2</title>
|
||||
<style>
|
||||
:root{
|
||||
--red:#b4232d;--red2:#d94a54;--red3:#fff1f2;--red4:#78202a;--red5:#5d1420;
|
||||
--cream:#fff8f6;--paper:#fffdfa;--bg:#f4f6f8;--white:#fff;--text:#20252d;--muted:#667085;
|
||||
--line:#ead7d3;--line2:#e5e7eb;--soft:#f8fafc;--amber:#b4690e;--green:#14804a;--blue:#1d5f99;
|
||||
--shadow:0 12px 36px rgba(31,41,55,.09);
|
||||
}
|
||||
*{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:"Microsoft YaHei","PingFang SC","Noto Sans CJK SC",Arial,sans-serif;color:var(--text);background:radial-gradient(circle at 20% 0%,rgba(180,35,45,.12),transparent 30%),linear-gradient(135deg,#f7eeee,#f6f7f9 45%,#fff7f5)}
|
||||
button,input,select{font:inherit}button{cursor:pointer}a{color:var(--red);text-decoration:none}.hidden{display:none!important}
|
||||
.app{min-height:100vh;display:flex;flex-direction:column}.header{height:82px;background:linear-gradient(120deg,var(--red5),var(--red));color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 30px;box-shadow:0 6px 22px rgba(126,22,32,.25);position:sticky;top:0;z-index:100}.brand{display:flex;align-items:center;gap:14px}.brand-mark{width:48px;height:48px;border:2px solid rgba(255,255,255,.75);border-radius:14px;display:grid;place-items:center;font-weight:900;font-size:22px;background:rgba(255,255,255,.12)}.brand h1{font-size:22px;margin:0 0 5px;letter-spacing:.3px}.brand p{font-size:12px;margin:0;color:rgba(255,255,255,.78)}.header-actions{display:flex;align-items:center;gap:10px}.search{position:relative}.search input{width:360px;border:1px solid rgba(255,255,255,.25);background:rgba(255,255,255,.15);color:#fff;border-radius:12px;padding:11px 42px 11px 15px;outline:none}.search input::placeholder{color:rgba(255,255,255,.72)}.search .icon{position:absolute;right:13px;top:10px;font-size:18px}.search-pop{position:absolute;right:0;top:50px;width:min(560px,92vw);max-height:480px;overflow:auto;background:#fff;color:var(--text);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);padding:8px;z-index:200}.search-item{display:flex;gap:12px;padding:12px;border-radius:10px;border-bottom:1px dashed #eee}.search-item:hover{background:var(--red3)}.search-kind{min-width:70px;color:var(--red);font-size:12px;font-weight:800}.search-item strong{display:block;font-size:14px;margin-bottom:4px}.search-item small{color:var(--muted);line-height:1.45}.btn{border:0;border-radius:10px;padding:10px 15px;font-weight:800;transition:.18s}.btn.white{background:#fff;color:var(--red)}.btn.ghost{background:rgba(255,255,255,.12);color:#fff;border:1px solid rgba(255,255,255,.25)}.btn.primary{background:var(--red);color:#fff}.btn.light{background:var(--red3);color:var(--red)}.btn.line{background:#fff;color:var(--red);border:1px solid #efb7bc}.btn.sm{padding:7px 10px;font-size:12px}.tabs{height:60px;background:#fff;display:flex;align-items:end;gap:2px;padding:0 24px;border-bottom:1px solid var(--line);position:sticky;top:82px;z-index:90;overflow-x:auto}.tab{border:0;background:transparent;color:#4b5563;font-weight:800;padding:18px 19px 15px;white-space:nowrap;border-bottom:3px solid transparent}.tab:hover{color:var(--red)}.tab.active{color:var(--red);border-bottom-color:var(--red)}.main{width:min(1540px,100%);margin:auto;padding:24px 28px 56px;flex:1}.page{display:none}.page.active{display:block}.page-head{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:20px}.page-head h2{margin:0 0 7px;font-size:26px;color:#531520}.page-head p{margin:0;color:var(--muted);line-height:1.65}.page-actions{display:flex;gap:9px;flex-wrap:wrap}.notice{background:linear-gradient(120deg,#fff4f4,#fff);border:1px solid #f2c6ca;border-left:5px solid var(--red);border-radius:13px;padding:14px 16px;line-height:1.75;color:#5e3034}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.stat{background:rgba(255,255,255,.94);border:1px solid var(--line);border-top:4px solid var(--red);border-radius:15px;padding:18px;box-shadow:0 6px 18px rgba(31,41,55,.045)}.stat .label{color:var(--muted);font-size:13px}.stat .num{font-size:30px;font-weight:900;margin:9px 0 3px;color:#54151e}.stat .hint{font-size:12px;color:#98a2b3}.grid{display:grid;gap:15px}.grid.cols2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid.cols3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.cols4{grid-template-columns:repeat(4,minmax(0,1fr))}.panel,.card{background:rgba(255,255,255,.96);border:1px solid var(--line);border-radius:16px;box-shadow:0 6px 22px rgba(31,41,55,.045);padding:18px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.panel h3,.panel h4{margin:0}.section-title{display:flex;justify-content:space-between;align-items:end;margin:25px 0 13px}.section-title h3{font-size:18px;margin:0;color:#5f1822}.section-title span{color:var(--muted);font-size:12px}.badge,.chip{display:inline-flex;align-items:center;border-radius:999px;padding:5px 9px;font-size:12px;font-weight:800;margin:2px 4px 2px 0}.badge.red,.chip.red{background:var(--red3);color:var(--red)}.badge.green{background:#e9f8ef;color:var(--green)}.badge.amber{background:#fff5e6;color:var(--amber)}.badge.blue{background:#e9f2fb;color:var(--blue)}.badge.gray{background:#f1f3f5;color:#667085}.filters{display:grid;grid-template-columns:1.4fr repeat(4,1fr) auto;gap:10px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:13px;margin-bottom:16px}.input,.select{width:100%;border:1px solid #d8dde4;border-radius:10px;padding:10px 11px;background:#fff;outline:none}.input:focus,.select:focus{border-color:var(--red);box-shadow:0 0 0 3px rgba(180,35,45,.08)}.card-item{background:#fff;border:1px solid var(--line);border-radius:16px;padding:17px;min-height:210px;display:flex;flex-directionLine truncated
|
||||
@media(max-width:1120px){.stats{grid-template-columns:repeat(2,1fr)}.grid.cols3,.grid.cols4{grid-template-columns:repeat(2,1fr)}.filters{grid-template-columns:1fr 1fr 1fr}.two-col{grid-template-columns:1fr}.detail-hero{grid-template-columns:1fr}}
|
||||
@media(max-width:760px){.header{height:auto;padding:14px 16px;align-items:flex-start}.brand p{display:none}.header-actions{display:none}.tabs{top:70px;padding-left:8px}.main{padding:17px 12px 45px}.stats,.grid.cols2,.grid.cols3,.grid.cols4{grid-template-columns:1fr}.filters{grid-template-columns:1fr}.modal-card{margin:3vh auto;max-height:94vh}.footer{display:block;line-height:1.8}.kv{grid-template-columns:1fr}.brand h1{font-size:18px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="header"><div class="brand"><div class="brand-mark">公</div><div><h1>台湾公务员群体知识数据展示系统</h1><p>基础画像 · 政策法规 · 研究成果 · 近月议题 · 动态监测</p></div></div><div class="header-actions"><div class="search"><input id="globalSearch" placeholder="搜索:考绩、年金、基层公务员、职场霸凌"><span class="icon">⌕</span><div id="searchPop" class="search-pop hidden"></div></div><button class="btn white" id="sourceBtn">来源索引</button><button class="btn ghost" id="resetBtn">重置</button></div></header>
|
||||
<nav class="tabs" id="tabs"></nav>
|
||||
<main class="main" id="main"></main>
|
||||
<footer class="footer"><span>单文件离线原型 v2 · 中文字段与富详情版</span><span>数据用途:公共治理研究与政策分析;不用于针对个体或群体的操纵性应用</span></footer>
|
||||
</div>
|
||||
<div id="modal" class="modal hidden"><div class="backdrop" onclick="closeModal()"></div><div class="modal-card"><button class="close" onclick="closeModal()">×</button><div id="modalBody"></div></div></div>
|
||||
<div id="detail" class="detail hidden"></div>
|
||||
<script>
|
||||
const EXTERNAL_JSON_URL = ""; // 后续可改为 "./taiwan_civil_service_display_data_v2.json"
|
||||
let DATA = {"meta": {"title": "台湾公务员群体知识数据展示系统", "version": "2.0-rich-ui", "data_update_time": "2026-06-23", "observation_period": "2026-05-24 至 2026-06-23", "scope": "台湾公务员群体基础画像、政策法规、研究成果、关注议题与近月动态", "source_note": "基于已整理的台湾公务员群体技能文档、政策文献、研究成果与近月公开资料构建。", "usage_note": "本页面用于公共治理研究、政策分析和资料展示。涉及群体敏感点时,统一表述为制度敏感点、治理风险点和政策观察点。", "ui_note": "v2 修复字段英文化、详情空白和研究成果内容过少问题;前端以中文字段和富文本详情展示。"}, "navigation": {"tabs": [{"id": "basic_knowledge", "label": "公务员基础知识", "description": "定义、数量、任用、官等职等、薪资、年龄、性别、法规"}, {"id": "policy_changes", "label": "政策法规变动", "description": "赖清德上任后涉公务员政策法规时间线"}, {"id": "research_outputs", "label": "报告与研究成果", "description": "研究成果、文献索引、核心发现和治理敏感点"}, {"id": "recent_issues", "label": "近月关注议题", "description": "近一个月关注议题的来龙去脉、诉求和态度结构"}, {"id": "monthly_dynamics", "label": "近月动态监测", "description": "新闻、组织、公务员社群、大V与官方动态"}]}, "global_query": {"enabled": true, "search_fields": ["title", "summary", "description", "keywords", "tags", "affected_groups", "source_type", "policy_name", "issue_name", "research_title", "actor", "event_title", "page_summary", "governance_sensitivity", "group_demands"], "filter_dimensions": ["date", "topic_type", "source_type", "affected_groups", "evidence_level", "importance", "status", "heat_level", "policy_type", "issue_type", "theme_cluster"]}, "data": {"basic_knowledge": {"tab_title": "公务员基础知识", "layout": "dashboard", "summary_cards": [{"id": "total_count", "label": "公务员总数", "value": 360080, "unit": "人", "description": "全國公務人員口径,民国114年/2025年"}, {"id": "average_age", "label": "平均年龄", "value": 43.75, "unit": "岁", "description": "民国114年/2025年统计"}, {"id": "male_ratio", "label": "男性占比", "value": "56.34%", "description": "全國公務人員性别结构"}, {"id": "female_ratio", "label": "女性占比", "value": "43.66%", "description": "全國公務人員性别结构"}], "definition": {"title": "台湾公务员群体定义", "summary": "台湾公务员通常指依法考试、任用、铨叙或特别人事制度进入公部门并承担公共行政职责的人员。页面采用全國公務人員总口径。", "included_groups": ["行政机关公务人员", "公营事业机构相关公务人员", "卫生医疗机构职员", "公立学校职员", "特殊职系公务人员"], "excluded_groups": ["政务人员", "民选地方行政首长", "民意代表", "公立学校教师"], "detail_display": "modal"}, "identity_comparison": {"title": "政务官与事务官区分", "display": "comparison_table", "items": [{"dimension": "基本性质", "political_official": "政治任命或政策领导人员", "career_civil_servant": "职业文官、常任公务人员"}, {"dimension": "进入方式", "political_official": "任命、特任、特派、提名同意", "career_civil_servant": "考试及格、铨叙合格、升等合格"}, {"dimension": "主要功能", "political_official": "制定政策、决定施政方向、承担政治责任", "career_civil_servant": "执行政策、处理行政事务、承担行政责任"}, {"dimension": "任期稳定性", "political_official": "通常随执政团队或政治任命变化而进退", "career_civil_servant": "具有身份保障,非因法定原因不得任意免职"}, {"dimension": "行政中立", "political_official": "一般不是行政中立法的主要适用对象", "career_civil_servant": "行政中立法的核心适用对象"}, {"dimension": "典型职位", "political_official": "部会首长、政务次长、政务委员", "career_civil_servant": "科员、专员、科长、司长、常务次长"}], "detail": {"政务人员": "政策制定层,承担政治责任,随执政团队和政治任命变化。", "事务官": "职业文官体系主体,强调依法行政、行政中立、身份保障和长期行政执行。", "边界类别": ["机要人员", "约聘雇人员", "行政契约人员"]}, "detail_display": "secondary_page"}, "appointment_modes": [{"name": "考试任用", "description": "通过国家考试进入公务体系,是事务官的典型进入路径。"}, {"name": "铨叙任用", "description": "依法取得任用资格并经铨叙程序确认。"}, {"name": "升等任用", "description": "依资历、考绩、训练或升等考试取得更高职等任用资格。"}, {"name": "政务任用", "description": "政治任命性质,主要对应Line truncated
|
||||
let activeTab = DATA.navigation.tabs[0].id;
|
||||
let filterState = {};
|
||||
const FIELD_LABELS = DATA.ui?.field_labels || {};
|
||||
const THEME_LABELS = DATA.ui?.theme_labels || {};
|
||||
const TAB_KIND = {basic_knowledge:'基础知识',policy_changes:'政策法规',research_outputs:'研究成果',recent_issues:'关注议题',monthly_dynamics:'动态监测'};
|
||||
function esc(x){return String(x ?? '').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
||||
function arr(x){return Array.isArray(x)?x:(x? [x]:[])}
|
||||
function label(k){return FIELD_LABELS[k] || THEME_LABELS[k] || k}
|
||||
function textOf(x){ if(x==null) return ''; if(typeof x==='string'||typeof x==='number'||typeof x==='boolean') return String(x); if(Array.isArray(x)) return x.map(textOf).join(' '); if(typeof x==='object') return Object.values(x).map(textOf).join(' '); return '' }
|
||||
function tagList(xs, cls='red'){ return arr(xs).map(x=>`<span class="badge ${cls}">${esc(label(x))}</span>`).join('') }
|
||||
function chips(xs){ return arr(xs).map(x=>`<span class="pill">${esc(label(x))}</span>`).join('') }
|
||||
function sourceButton(url){ return url ? `<a class="source-link" target="_blank" href="${esc(url)}">打开来源</a>` : `<span class="badge gray">暂无来源链接</span>` }
|
||||
function detailBlocks(sections){ return arr(sections).filter(s=>textOf(s.content).trim()).map(s=>`<div class="detail-block"><h4>${esc(s.title)}</h4><p>${esc(s.content)}</p></div>`).join('') }
|
||||
function kv(labelText, value){ if(value==null || value==='' || (Array.isArray(value)&&!value.length)) return ''; return `<div class="kv"><div class="k">${esc(labelText)}</div><div class="v">${Array.isArray(value)?tagList(value):esc(label(value))}</div></div>`}
|
||||
function setup(){renderTabs(); renderPage(); bindGlobalSearch(); document.getElementById('resetBtn').onclick=()=>{filterState={}; document.getElementById('globalSearch').value=''; renderPage();}; document.getElementById('sourceBtn').onclick=showSources;}
|
||||
function renderTabs(){const box=document.getElementById('tabs'); box.innerHTML=DATA.navigation.tabs.map(t=>`<button class="tab ${t.id===activeTab?'active':''}" data-id="${t.id}">${esc(t.label)}</button>`).join(''); box.querySelectorAll('button').forEach(b=>b.onclick=()=>{activeTab=b.dataset.id; filterState={}; document.getElementById('searchPop').classList.add('hidden'); renderTabs(); renderPage();});}
|
||||
function pageHead(title,desc){return `<div class="page-head"><div><h2>${esc(title)}</h2><p>${esc(desc||'')}</p></div><div class="page-actions"><button class="btn line sm" onclick="showSources()">来源索引</button><button class="btn primary sm" onclick="exportCurrentJSON()">导出本栏JSON</button></div></div>`}
|
||||
function renderPage(){const main=document.getElementById('main'); const tab=DATA.navigation.tabs.find(t=>t.id===activeTab); let html=pageHead(tab.label, tab.description); if(activeTab==='basic_knowledge') html+=renderBasic(); if(activeTab==='policy_changes') html+=renderPolicy(); if(activeTab==='research_outputs') html+=renderResearch(); if(activeTab==='recent_issues') html+=renderIssues(); if(activeTab==='monthly_dynamics') html+=renderDynamics(); main.innerHTML=html; bindFilters();}
|
||||
function renderBasic(){const d=DATA.data.basic_knowledge; const cards=d.summary_cards||[]; let html=`<div class="notice mb">${esc(d.rich_note?.paragraphs?.[0]||'')}</div>`; html+=`<div class="stats mb">${cards.map(c=>`<div class="stat"><div class="label">${esc(c.label)}</div><div class="num">${esc(c.value)}${c.unit?`<small style="font-size:14px"> ${esc(c.unit)}</small>`:''}</div><div class="hint">${esc(c.description||'')}</div></div>`).join('')}</div>`; html+=`<div class="grid cols2"><div class="panel"><div class="panel-head"><h3>台湾公务员群体定义</h3><button class="btn light sm" onclick="openBasicDetail('definition')">详情</button></div><p>${esc(d.definition.summary||d.definition.official_scope||'')}</p><h4>包含群体</h4><div>${tagList(d.definition.included_groups,'green')}</div><h4 class="mt">排除群体</h4><div>${tagList(d.definition.excluded_groups,'gray')}</div></div>`;
|
||||
html+=`<div class="panel"><div class="panel-head"><h3>政务官与事务官区分</h3><button class="btn light sm" onclick="openBasicDetail('identity')">二级详情</button></div><div class="table-wrap"><table class="compare-table"><thead><tr><th>比较维度</th><th>政务官 / 政务人员</th><th>事务官 / 常任文官</th></tr></thead><tbody>${(d.identity_comparison.items||[]).map(r=>`<tr><td>${esc(r.dimension)}</td><td>${esc(r.political_official)}</td><td>${esc(r.career_civil_servant)}</td></tr>`).join('')}</tbody></table></div></div></div>`;
|
||||
html+=`<div class="section-title"><h3>任用方式与官等职等</h3><span>点击卡片可查看说明</span></div><div class="grid cols3">${(d.appointment_modes||[]).map(m=>`<div class="card-item" onclick="openModalHTML('${esc(m.name)}','<p>${esc(m.description)}</p>')"><h4>${esc(m.name)}</h4><p>${esc(m.description)}</p></div>`).join('')}</div>`;
|
||||
html+=`<div class="grid cols2 mt"><div class="panel"><div class="panel-head"><h3>官等职等结构</h3></div>${(d.rank_structure||[]).map(r=>`<div class="rank"><strong>${esc(r.rank)}</strong><p>${esc(r.grade_range)}|${esc(arr(r.typical_positions).join('、'))}</p></div>`).join('')}</div><div class="panel"><div class="panel-head"><h3>薪资待遇结构</h3></div>${(d.salary_structure||[]).map(s=>`<div class="detail-block"><h4>${esc(s.name)}</h4><p>${esc(s.description)}</p></div>`).join('')}</div></div>`;
|
||||
html+=`<div class="grid cols2 mt"><div class="panel"><div class="panel-head"><h3>年龄结构</h3><span class="badge red">平均 ${esc(d.summary_cards?.find(c=>c.id==='average_age')?.value||'43.75')} 岁</span></div>${(d.age_structure||[]).map(a=>`<div class="bar-row"><span>${esc(a.age_group)}</span><div class="bar-track"><div class="bar-fill" style="width:${Math.max(2,a.ratio*4)}%"></div></div><b>${esc(a.ratio)}%</b></div>`).join('')}</div><div class="panel"><div class="panel-head"><h3>性别结构与法规框架</h3></div>${(d.gender_structure||[]).map(g=>`<div class="bar-row"><span>${esc(g.gender)}</span><div class="bar-track"><div class="bar-fill" style="width:${g.ratio}%"></div></div><b>${g.ratio}%</b></div>`).join('')}<h4>适用法规</h4><div class="law-grid">${tagList(d.legal_framework,'blue')}</div></div></div>`;
|
||||
html+=`<div class="panel mt"><h3>结构性判断</h3><div class="grid cols3 mt">${(d.structure_judgement||[]).map(j=>`<div class="detail-block"><h4>${esc(j.label||j.observation||'观察项')}</h4><p>${esc(j.explanation||j.description||j.value||textOf(j))}</p></div>`).join('')}</div></div>`;
|
||||
return html;}
|
||||
function openBasicDetail(kind){const d=DATA.data.basic_knowledge; if(kind==='definition'){openModalHTML('台湾公务员群体定义', `<p>${esc(d.definition.summary||'')}</p><h4>包含群体</h4><div>${tagList(d.definition.included_groups,'green')}</div><h4>排除群体</h4><div>${tagList(d.definition.excluded_groups,'gray')}</div>${detailBlocks((d.rich_note?.paragraphs||[]).map((p,i)=>({title:'说明 '+(i+1),content:p})))}`)} else {openDetailPage('政务官与事务官区分', d.identity_comparison.title, `<div class="notice">政务官偏向政治任命与政策责任,事务官偏向考试任用、行政执行与身份保障。机要人员和约聘雇人员应作为边界类别单独识别。</div><div class="table-wrap mt"><table><thead><tr><th>比较维度</th><th>政务官 / 政务人员</th><th>事务官 / 常任文官</th></tr></thead><tbody>${d.identity_comparison.items.map(r=>`<tr><td>${esc(r.dimension)}</td><td>${esc(r.political_official)}</td><td>${esc(r.career_civil_servant)}</td></tr>`).join('')}</tbody></table></div>`)}}
|
||||
function filterUI(type, selects){return `<div class="filters"><input class="input" data-filter="q" placeholder="在本栏搜索关键词" value="${esc(filterState.q||'')}">${selects.map(s=>selectHTML(s.key,s.label,s.options)).join('')}<button class="btn primary" onclick="applyFilters()">查询</button></div>`}
|
||||
function selectHTML(key,labelText,options){return `<select class="select" data-filter="${key}"><option value="">${esc(labelText)}:全部</option>${options.map(o=>`<option ${filterState[key]===o?'selected':''} value="${esc(o)}">${esc(label(o))}</option>`).join('')}</select>`}
|
||||
function bindFilters(){document.querySelectorAll('[data-filter]').forEach(el=>{el.onchange=()=>{filterState[el.dataset.filter]=el.value; renderPage();}; el.onkeydown=e=>{if(e.key==='Enter'){filterState[el.dataset.filter]=el.value; renderPage();}}});}
|
||||
function applyFilters(){document.querySelectorAll('[data-filter]').forEach(el=>filterState[el.dataset.filter]=el.value); renderPage();}
|
||||
function unique(items, getter){return [...new Set(items.flatMap(x=>arr(getter(x))).filter(Boolean))]}
|
||||
function filterList(items, cfg){let q=(filterState.q||'').trim().toLowerCase(); return items.filter(it=>{if(q && !textOf(it).toLowerCase().includes(q)) return false; for(const [k,getter] of Object.entries(cfg||{})){if(filterState[k]){let vals=arr(getter(it)).map(String); if(!vals.includes(filterState[k])) return false;}} return true;})}
|
||||
function renderPolicy(){const d=DATA.data.policy_changes, items=d.items||[]; const filtered=filterList(items,{type:x=>x.policy_type,status:x=>x.status,group:x=>x.affected_groups,importance:x=>x.importance}); let html=filterUI('policy',[{key:'type',label:'政策类型',options:unique(items,x=>x.policy_type)},{key:'status',label:'政策状态',options:unique(items,x=>x.status)},{key:'group',label:'影响群体',options:unique(items,x=>x.affected_groups)},{key:'importance',label:'重要性',options:unique(items,x=>x.importance)}]); html+=`<div class="stats mb"><div class="stat"><div class="label">政策总数</div><div class="num">${items.length}</div><div class="hint">赖清德上任至今</div></div><div class="stat"><div class="label">当前命中</div><div class="num">${filtered.length}</div><div class="hint">按筛选条件</div></div><div class="stat"><div class="label">高重要性</div><div class="num">${items.filter(x=>x.importance==='高').length}</div><div class="hint">重点跟踪</div></div><div class="stat"><div class="label">草案/审议</div><div class="num">${items.filter(x=>/草案|审议|函送/.test(x.status+x.summary)).length}</div><div class="hint">需后续观察</div></div></div>`; html+=`<div class="grid cols2"><div class="panel"><h3>政策法规时间线</h3><div class="timeline mt">${filtered.map(it=>eventHTML(it.date,it.policy_name,it.summary,`openPolicy('${it.id}')`)).join('')||'<div class="empty">无匹配数据</div>'}</div></div><div class="panel"><h3>政策卡片</h3><div class="grid cols2 mt">${filtered.map(policyCard).join('')||'<div class="empty">无匹配数据</div>'}</div></div></div>`; return html;}
|
||||
function eventHTML(date,title,summary,click){return `<div class="event" onclick="${click}"><div class="date">${esc(date)}</div><h4>${esc(title)}</h4><p>${esc(summary)}</p></div>`}
|
||||
function policyCard(it){return `<div class="card-item"><h4>${esc(it.policy_name)}</h4><div>${tagList([it.policy_type,it.status,it.importance])}</div><p>${esc(it.summary)}</p><div>${tagList(it.affected_groups,'gray')}</div><div class="card-foot"><button class="btn light sm" onclick="openPolicy('${it.id}')">弹框详情</button><button class="btn line sm" onclick="openPolicyPage('${it.id}')">二级详情</button></div></div>`}
|
||||
function openPolicy(id){const it=DATA.data.policy_changes.items.find(x=>x.id===id); openModalHTML(it.policy_name, renderRecord(it, 'policy'))}
|
||||
function openPolicyPage(id){const it=DATA.data.policy_changes.items.find(x=>x.id===id); openDetailPage('政策法规二级详情', it.policy_name, renderRecord(it,'policy',true))}
|
||||
function renderResearch(){const d=DATA.data.research_outputs, items=d.items||[]; const filtered=filterList(items,{theme:x=>x.theme_cluster,source:x=>x.source_type,evidence:x=>x.evidence_strength,group:x=>x.target_groups}); let html=filterUI('research',[{key:'theme',label:'主题簇',options:unique(items,x=>x.theme_cluster)},{key:'source',label:'来源类型',options:unique(items,x=>x.source_type)},{key:'evidence',label:'证据强度',options:unique(items,x=>x.evidence_strength)},{key:'group',label:'对应群体',options:unique(items,x=>x.target_groups)}]); html+=`<div class="grid cols4 mb">${(d.theme_clusters||[]).map(c=>`<div class="panel" onclick="filterState.theme='${esc(c.id)}'; renderPage();"><h4>${esc(c.label)}</h4><p>${esc((c.keywords||[]).join('、'))}</p><span class="badge red">${esc(c.count||items.filter(x=>x.theme_cluster===c.id).length)} 条</span></div>`).join('')}</div>`; html+=`<div class="grid cols3">${filtered.map(researchCard).join('')||'<div class="empty">无匹配数据</div>'}</div>`; html+=`<div class="section-title"><h3>研究结论图谱</h3><span>点击查看支撑研究</span></div><div class="grid cols3">${(d.insight_map||[]).map(i=>`<div class="panel"><h4>${esc(i.theme)}</h4><p>${esc(i.core_conclusion)}</p><div>${tagList(i.related_groups,'gray')}</div><button class="btn light sm mt" onclick="openInsight('${esc(i.theme)}')">查看关联</button></div>`).join('')}</div>`; return html;}
|
||||
function researchCard(it){return `<div class="card-item"><h4>${esc(it.title)}</h4><div>${tagList([THEME_LABELS[it.theme_cluster]||it.theme_cluster,it.source_type,it.evidence_strength])}</div><p>${esc(it.page_summary||it.core_finding)}</p><div>${tagList(it.target_groups,'gray')}</div><div class="card-foot"><button class="btn light sm" onclick="openResearch('${it.id}')">弹框详情</button><button class="btn line sm" onclick="openResearchPage('${it.id}')">二级详情</button></div></div>`}
|
||||
function openResearch(id){const it=DATA.data.research_outputs.items.find(x=>x.id===id); openModalHTML(it.title, renderRecord(it,'research') + `<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function openResearchPage(id){const it=DATA.data.research_outputs.items.find(x=>x.id===id); openDetailPage('研究成果二级详情', it.title, renderRecord(it,'research',true)+`<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function openInsight(theme){const i=DATA.data.research_outputs.insight_map.find(x=>x.theme===theme); const rs=arr(i.related_research_ids).map(id=>DATA.data.research_outputs.items.find(x=>x.id===id)).filter(Boolean); openModalHTML(theme, `<p class="modal-sub">${esc(i.core_conclusion)}</p><div>${tagList(i.related_groups,'gray')}</div><div class="grid cols2 mt">${rs.map(researchCard).join('')}</div>`)}
|
||||
function renderIssues(){const d=DATA.data.recent_issues, items=d.items||[]; const filtered=filterList(items,{heat:x=>x.heat_level,type:x=>x.issue_type,group:x=>x.affected_groups,evidence:x=>x.evidence_level}); let html=filterUI('issues',[{key:'heat',label:'热度',options:unique(items,x=>x.heat_level)},{key:'type',label:'议题类型',options:unique(items,x=>x.issue_type)},{key:'group',label:'影响群体',options:unique(items,x=>x.affected_groups)},{key:'evidence',label:'证据等级',options:unique(items,x=>x.evidence_level)}]); html+=`<div class="notice mb">近月议题栏展示的是“事件链—群体诉求—态度结构—制度敏感点”,不是简单榜单。</div><div class="grid cols3">${filtered.map(issueCard).join('')||'<div class="empty">无匹配数据</div>'}</div>`; const dm=d.demand_map||[]; if(dm.length) html+=`<div class="section-title"><h3>群体诉求图谱</h3></div><div class="grid cols4">${dm.map(x=>`<div class="panel"><h4>${esc(x.demand_type||x.type||x.title)}</h4><p>${esc(x.content||x.description||textOf(x))}</p></div>`).join('')}</div>`; return html;}
|
||||
function issueCard(it){return `<div class="card-item"><h4>${esc(it.issue_name)}</h4><div>${tagList([it.heat_level,it.issue_type,it.evidence_level])}</div><p>${esc(it.page_summary)}</p><div>${tagList(it.affected_groups,'gray')}</div><div class="card-foot"><button class="btn light sm" onclick="openIssue('${it.id}')">弹框详情</button><button class="btn line sm" onclick="openIssuePage('${it.id}')">二级详情</button></div></div>`}
|
||||
function openIssue(id){const it=DATA.data.recent_issues.items.find(x=>x.id===id); openModalHTML(it.issue_name, renderRecord(it,'issue') + `<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function openIssuePage(id){const it=DATA.data.recent_issues.items.find(x=>x.id===id); openDetailPage('议题深度二级详情', it.issue_name, renderRecord(it,'issue',true)+`<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function renderDynamics(){const d=DATA.data.monthly_dynamics, items=d.items||[]; const filtered=filterList(items,{source:x=>x.source_type,issue:x=>x.related_issue,importance:x=>x.importance,evidence:x=>x.evidence_level}); let html=filterUI('dynamics',[{key:'source',label:'来源类型',options:unique(items,x=>x.source_type)},{key:'issue',label:'关联议题',options:unique(items,x=>x.related_issue)},{key:'importance',label:'重要性',options:unique(items,x=>x.importance)},{key:'evidence',label:'证据等级',options:unique(items,x=>x.evidence_level)}]); html+=`<div class="grid cols2"><div class="panel"><h3>动态时间线</h3><div class="timeline mt">${filtered.map(it=>eventHTML(it.date,it.event_title,it.event_summary,`openDynamic('${it.id}')`)).join('')||'<div class="empty">无匹配数据</div>'}</div></div><div class="panel"><h3>月度综合判断</h3>${(d.monthly_summary?.main_lines||[]).map(x=>`<div class="detail-block"><h4>${esc(x.line)}</h4><p>${esc(x.summary)}</p></div>`).join('')}</div></div>`; html+=`<div class="section-title"><h3>动态卡片流</h3><span>${filtered.length} 条</span></div><div class="grid cols3">${filtered.map(dynamicCard).join('')}</div>`; return html;}
|
||||
function dynamicCard(it){return `<div class="card-item"><h4>${esc(it.event_title)}</h4><div>${tagList([it.date,it.source_type,it.importance])}</div><p>${esc(it.page_summary||it.event_summary)}</p><div>${tagList(it.related_issue,'gray')}</div><div class="card-foot"><button class="btn light sm" onclick="openDynamic('${it.id}')">弹框详情</button><button class="btn line sm" onclick="openDynamicPage('${it.id}')">二级详情</button></div></div>`}
|
||||
function openDynamic(id){const it=DATA.data.monthly_dynamics.items.find(x=>x.id===id); openModalHTML(it.event_title, renderRecord(it,'dynamic') + `<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function openDynamicPage(id){const it=DATA.data.monthly_dynamics.items.find(x=>x.id===id); openDetailPage('动态监测二级详情', it.event_title, renderRecord(it,'dynamic',true)+`<div class="mt">${sourceButton(it.source_url)}</div>`)}
|
||||
function renderRecord(it, type, full=false){let top=''; if(type==='policy') top+=kv('编号',it.id)+kv('日期',it.date)+kv('政策类型',it.policy_type)+kv('政策状态',it.status)+kv('主管机关',it.authority)+kv('重要性',it.importance)+kv('影响群体',it.affected_groups); if(type==='research') top+=kv('编号',it.id)+kv('来源类型',it.source_type)+kv('机构',it.institution)+kv('作者',it.author)+kv('年份',it.year)+kv('文献类型',it.document_type)+kv('主题簇',THEME_LABELS[it.theme_cluster]||it.theme_cluster)+kv('对应群体',it.target_groups)+kv('证据强度',it.evidence_strength); if(type==='issue') top+=kv('编号',it.id)+kv('热度',it.heat_level)+kv('议题类型',it.issue_type)+kv('时间范围',it.time_range)+kv('影响群体',it.affected_groups)+kv('证据等级',it.evidence_level); if(type==='dynamic') top+=kv('编号',it.id)+kv('日期',it.date)+kv('来源类型',it.source_type)+kv('主体',it.actor)+kv('重要性',it.importance)+kv('关联议题',it.related_issue)+kv('影响群体',it.affected_groups)+kv('证据等级',it.evidence_level);
|
||||
let html=`<div class="detail-block"><h4>基本信息</h4>${top||'<p>无基本信息</p>'}</div>`; html += detailBlocks(it.rich_detail||[]);
|
||||
if(it.event_chain){ html+=`<div class="detail-block"><h4>事件链</h4><div class="timeline">${it.event_chain.map(e=>eventHTML(e.date,e.event,e.event,'')).join('')}</div></div>`}
|
||||
if(it.attitude_structure){ html+=`<div class="detail-block"><h4>态度结构</h4>${it.attitude_structure.map(a=>`<p><b>${esc(a.attitude)}:</b>${esc(a.description)}</p>`).join('')}</div>`}
|
||||
if(it.tags) html += `<div class="detail-block"><h4>标签</h4>${tagList(it.tags,'gray')}</div>`; return html;}
|
||||
function openModalHTML(title, html){document.getElementById('modalBody').innerHTML=`<h2 class="modal-title">${title}</h2>${html}`; document.getElementById('modal').classList.remove('hidden')}
|
||||
function closeModal(){document.getElementById('modal').classList.add('hidden')}
|
||||
function openDetailPage(kind,title,html){document.getElementById('detail').innerHTML=`<div class="detail-top"><strong>${esc(kind)}</strong><button class="btn white" onclick="closeDetail()">返回</button></div><div class="detail-body"><div class="detail-hero"><div><h2>${esc(title)}</h2><p>本页用于展示较长字段、事件链、研究解释和关联信息,避免主列表过度拥挤。</p></div><div class="detail-summary"><b>展示原则</b><p>主卡片只显示摘要;本页显示完整结构化内容。字段均已中文化,不再直接展示技术字段名。</p></div></div><div class="mt">${html}</div></div>`; document.getElementById('detail').classList.remove('hidden')}
|
||||
function closeDetail(){document.getElementById('detail').classList.add('hidden')}
|
||||
function showSources(){const sources=DATA.source_index||[]; openModalHTML('来源索引', `<p class="modal-sub">本页保留来源索引,便于后续回溯和外部 JSON 长期更新。</p>${sources.map(s=>`<div class="source-card"><h4>${esc(s.name)}</h4><p>${esc(s.type||'')}|${esc(s.note||'')}</p><div class="mt">${sourceButton(s.url)}</div></div>`).join('')}`)}
|
||||
function exportCurrentJSON(){const obj=DATA.data[activeTab]; const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=activeTab+'.json'; a.click(); URL.revokeObjectURL(a.href)}
|
||||
function bindGlobalSearch(){const inp=document.getElementById('globalSearch'), pop=document.getElementById('searchPop'); inp.addEventListener('input',()=>{const q=inp.value.trim().toLowerCase(); if(!q){pop.classList.add('hidden');return;} const rows=[]; for(const tab of DATA.navigation.tabs){const sec=DATA.data[tab.id]; let items=[]; if(tab.id==='basic_knowledge'){items=[{id:'basic',title:'公务员基础知识',summary:textOf(sec),tab:tab.id}];} else items=sec.items||[]; items.forEach(it=>{if(textOf(it).toLowerCase().includes(q)){rows.push({tab:tab.id, kind:tab.label, item:it, title:itemTitle(it,tab.id), summary:itemSummary(it,tab.id)});}})} pop.innerHTML=rows.slice(0,30).map((r,i)=>`<div class="search-item" onclick="openSearchResult('${r.tab}','${esc(r.item.id||'basic')}')"><div class="search-kind">${esc(r.kind)}</div><div><strong>${esc(r.title)}</strong><small>${esc(r.summary).slice(0,120)}</small></div></div>`).join('') || '<div class="empty">无匹配结果</div>'; pop.classList.remove('hidden');});}
|
||||
function itemTitle(it,tab){return it.policy_name||it.title||it.issue_name||it.event_title||'基础知识'} function itemSummary(it,tab){return it.summary||it.page_summary||it.core_finding||it.event_summary||textOf(it)}
|
||||
function openSearchResult(tab,id){activeTab=tab; filterState={}; renderTabs(); renderPage(); document.getElementById('searchPop').classList.add('hidden'); if(tab==='policy_changes') openPolicy(id); else if(tab==='research_outputs') openResearch(id); else if(tab==='recent_issues') openIssue(id); else if(tab==='monthly_dynamics') openDynamic(id); else openBasicDetail('definition')}
|
||||
async function tryExternal(){ if(!EXTERNAL_JSON_URL) return; try{const r=await fetch(EXTERNAL_JSON_URL); if(r.ok){DATA=await r.json();}}catch(e){console.warn('外部JSON读取失败,继续使用内置数据',e);} }
|
||||
tryExternal().then(setup);
|
||||
</script>
|
||||
</body></html>
|
||||
Reference in new issue
Block a user