Spaces:
Sleeping
Sleeping
<html lang="zh-TW"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>防中相關立法資訊檢索系統</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script> | |
<style> | |
/* 標題與歡迎訊息 */ | |
.header { | |
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); | |
color: white; | |
border-radius: 12px; | |
padding: 30px; | |
margin-bottom: 30px; | |
text-align: center; | |
} | |
.header h1 { | |
font-size: 2.5em; | |
margin-bottom: 15px; | |
font-weight: 600; | |
} | |
.header p { | |
font-size: 1.2em; | |
opacity: 0.9; | |
} | |
/* 導覽按鈕 */ | |
.nav-buttons { | |
display: flex; | |
justify-content: center; | |
gap: 15px; | |
margin-bottom: 30px; | |
flex-wrap: wrap; | |
} | |
.nav-btn { | |
background: white; | |
color: #3498db; | |
padding: 12px 25px; | |
border: 2px solid #3498db; | |
border-radius: 25px; | |
text-decoration: none; | |
font-weight: 500; | |
transition: all 0.3s; | |
} | |
.nav-btn:hover { | |
background: #3498db; | |
color: white; | |
transform: translateY(-2px); | |
} | |
/* 系統使用說明 - 改進樣式 */ | |
.guide { | |
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | |
border-radius: 12px; | |
padding: 20px; | |
margin-bottom: 30px; | |
border-left: 4px solid #3498db; | |
transition: all 0.3s; | |
} | |
.guide.collapsed { | |
background: linear-gradient(135deg, #f8f9fa 0%, #f0f0f0 100%); | |
border-left-color: #dee2e6; | |
} | |
.guide h3 { | |
color: #2c3e50; | |
margin-bottom: 15px; | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
font-size: 18px; | |
font-weight: 600; | |
} | |
.guide-toggle-btn { | |
background: none; | |
border: none; | |
color: #3498db; | |
cursor: pointer; | |
font-size: 14px; | |
font-weight: 500; | |
transition: color 0.3s; | |
} | |
.guide-toggle-btn:hover { | |
color: #2980b9; | |
} | |
.guide-content { | |
display: grid; | |
gap: 15px; | |
} | |
.guide-item { | |
display: flex; | |
align-items: flex-start; | |
gap: 12px; | |
} | |
.guide-item-bullet { | |
font-weight: bold; | |
color: #3498db; | |
font-size: 16px; | |
} | |
.guide-item-content { | |
flex: 1; | |
} | |
.guide-item strong { | |
color: #2c3e50; | |
font-weight: 600; | |
} | |
.guide-highlight { | |
color: #e74c3c; | |
font-weight: 700; | |
} | |
.guide-sub-items { | |
margin-left: 20px; | |
margin-top: 8px; | |
padding: 10px; | |
background: rgba(52, 152, 219, 0.1); | |
border-radius: 8px; | |
font-size: 13px; | |
line-height: 1.8; | |
} | |
.guide-tag { | |
background-color: #e74c3c; | |
color: white; | |
padding: 2px 6px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 600; | |
} | |
/* 確保圖標大小正確 */ | |
.lucide { | |
width: 1em; | |
height: 1em; | |
vertical-align: middle; | |
} | |
/* 時間軸樣式 */ | |
.timeline-line { | |
position: absolute; | |
left: 2rem; | |
top: 0; | |
bottom: 0; | |
width: 2px; | |
background-color: #d1d5db; | |
} | |
.timeline-node { | |
position: absolute; | |
left: 1.5rem; | |
width: 1rem; | |
height: 1rem; | |
background-color: white; | |
border-radius: 50%; | |
border: 2px solid #9ca3af; | |
z-index: 10; | |
} | |
/* 動畫效果 */ | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
.animate-spin { | |
animation: spin 1s linear infinite; | |
} | |
/* 捲軸樣式 */ | |
::-webkit-scrollbar { | |
width: 8px; | |
height: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: #f3f4f6; | |
} | |
::-webkit-scrollbar-thumb { | |
background: #d1d5db; | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: #9ca3af; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<!-- 載入畫面 --> | |
<div id="loadingScreen" class="min-h-screen bg-gray-50 flex items-center justify-center"> | |
<div class="text-center"> | |
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div> | |
<p class="mt-4 text-gray-600">載入中...</p> | |
</div> | |
</div> | |
<!-- 主要內容(初始隱藏) --> | |
<div id="mainContent" class="hidden"> | |
<!-- 法律列表頁面 --> | |
<div id="lawListPage"> | |
<!-- 頁首 --> | |
<header class="bg-white shadow-sm border-b"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
<!-- 標題與歡迎訊息 --> | |
<div class="header"> | |
<h1>⚖️ 防中相關立法資訊檢索系統</h1> | |
<p>提供防止中國滲透相關法律制定與修正歷程查詢服務</p> | |
</div> | |
</div> | |
</header> | |
<!-- 導覽按鈕 --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
<div class="nav-buttons"> | |
<a href="systems.html" class="nav-btn">🏠 首頁</a> | |
<a href="選舉系統.html" class="nav-btn">🗳️ 選舉分析</a> | |
<a href="NCC裁罰分析系統.html" class="nav-btn">📊 NCC分析</a> | |
</div> | |
</div> | |
<!-- 系統使用說明 --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> | |
<div id="guideContainer" class="guide"> | |
<div class="flex items-center justify-between"> | |
<h3> | |
<i data-lucide="info" class="h-5 w-5"></i> | |
系統使用說明 | |
</h3> | |
<button onclick="toggleGuide()" class="guide-toggle-btn"> | |
<span id="guideToggleText">收起</span> | |
</button> | |
</div> | |
<div id="guideContent" class="guide-content"> | |
<div class="guide-item"> | |
<span class="guide-item-bullet">•</span> | |
<div class="guide-item-content"> | |
<strong>搜尋功能</strong>:可使用上方搜尋框查詢法律名稱、議案編號、提案人等資訊。搜尋到議案時會顯示在「相關議案搜尋結果」區塊,點擊「查看詳細」可直接跳到該議案 | |
</div> | |
</div> | |
<div class="guide-item"> | |
<span class="guide-item-bullet">•</span> | |
<div class="guide-item-content"> | |
<strong class="guide-highlight">防中議案篩選(重要功能)</strong>: | |
<div class="guide-sub-items"> | |
- 首頁法律卡片上的紅色<span class="guide-tag">防中議案</span>標籤<strong>可點擊</strong><br/> | |
- 點擊後會進入該法律頁面並<strong>自動篩選只顯示防中議案</strong><br/> | |
- 在法律詳細頁面右上角也有防中議案按鈕,可隨時切換篩選<br/> | |
- 系統會根據不同法律使用對應的關鍵字進行智能標示 | |
</div> | |
</div> | |
</div> | |
<div class="guide-item"> | |
<span class="guide-item-bullet">•</span> | |
<div class="guide-item-content"> | |
<strong>分類瀏覽</strong>:使用分類標籤快速篩選「媒體相關」或「國安相關」法律 | |
</div> | |
</div> | |
<div class="guide-item"> | |
<span class="guide-item-bullet">•</span> | |
<div class="guide-item-content"> | |
<strong>時間軸檢視</strong>:查看完整立法歷程,點擊時間軸上的「查看議案」可展開詳細資訊 | |
</div> | |
</div> | |
<div class="guide-item"> | |
<span class="guide-item-bullet">•</span> | |
<div class="guide-item-content"> | |
<strong>議案附件</strong>:可直接查看或下載相關文書(PDF、DOC) | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 搜尋區塊 --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
<div class="bg-white rounded-lg shadow p-6"> | |
<div class="flex flex-col md:flex-row gap-4"> | |
<div class="flex-1"> | |
<div class="relative"> | |
<i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5"></i> | |
<input | |
type="text" | |
id="searchInput" | |
placeholder="搜尋法律名稱、編號、議案編號、提案人、連署人等..." | |
class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
onkeyup="handleSearchInput(event)" | |
/> | |
</div> | |
</div> | |
<select | |
id="searchField" | |
class="px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
onchange="performSearch()" | |
> | |
<option value="all">不分欄位</option> | |
<option value="name">法律名稱</option> | |
<option value="billName">議案名稱</option> | |
<option value="proposer">提案人/連署人</option> | |
<option value="billNumber">議案編號</option> | |
<option value="description">說明</option> | |
</select> | |
</div> | |
<div class="mt-3 text-sm text-gray-600" id="searchStats"></div> | |
</div> | |
</div> | |
<!-- 法律列表和議案搜尋結果 --> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12"> | |
<!-- 議案搜尋結果 --> | |
<div id="billSearchResults" class="mb-6 hidden"> | |
<h2 class="text-lg font-semibold text-gray-900 mb-4">相關議案搜尋結果</h2> | |
<div class="space-y-4" id="billSearchResultsList"></div> | |
</div> | |
<!-- 分類標籤 --> | |
<div class="mb-6 bg-white rounded-lg shadow p-4"> | |
<div class="flex flex-wrap gap-2"> | |
<button | |
onclick="setActiveCategory('all')" | |
id="cat-all" | |
class="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-blue-600 text-white" | |
> | |
全部法律 (<span id="countAll">0</span>) | |
</button> | |
<div class="flex items-center gap-2 ml-4"> | |
<span class="text-sm text-gray-500">已通過:</span> | |
<button | |
onclick="setActiveCategory('passed-media')" | |
id="cat-passed-media" | |
class="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-green-100 text-green-700 hover:bg-green-200" | |
> | |
媒體相關 (<span id="countPassedMedia">0</span>) | |
</button> | |
<button | |
onclick="setActiveCategory('passed-security')" | |
id="cat-passed-security" | |
class="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-red-100 text-red-700 hover:bg-red-200" | |
> | |
國安相關 (<span id="countPassedSecurity">0</span>) | |
</button> | |
</div> | |
<div class="flex items-center gap-2 ml-4"> | |
<span class="text-sm text-gray-500">尚未通過:</span> | |
<button | |
onclick="setActiveCategory('pending')" | |
id="cat-pending" | |
class="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-yellow-100 text-yellow-700 hover:bg-yellow-200" | |
> | |
審議中 (<span id="countPending">0</span>) | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- 法律卡片列表 --> | |
<div class="grid gap-4" id="lawCardsList"></div> | |
</div> | |
</div> | |
<!-- 法律詳細頁面 --> | |
<div id="lawDetailPage" class="hidden"> | |
<!-- 詳細頁面內容將動態生成 --> | |
</div> | |
</div> | |
<script> | |
// 全域變數 | |
let laws = []; | |
let billsData = []; | |
let searchResults = { laws: [], bills: [] }; | |
let selectedLaw = null; | |
let showGuide = true; | |
let activeCategory = 'all'; | |
let detailSearchTerm = ''; | |
let showOnlyAntiChina = false; | |
let versions = []; | |
let bills = []; | |
let progressData = []; | |
let activeTab = 'timeline'; | |
// 初始化 | |
document.addEventListener('DOMContentLoaded', async function() { | |
await loadData(); | |
lucide.createIcons(); | |
}); | |
// 載入資料 | |
async function loadData() { | |
try { | |
// 載入法律基本資料 | |
const basicResponse = await fetch('法律基本資料.csv'); | |
const basicText = await basicResponse.text(); | |
const parsedBasic = Papa.parse(basicText, { | |
header: true, | |
dynamicTyping: true, | |
skipEmptyLines: true | |
}); | |
// 載入法律詳細數據 | |
const detailResponse = await fetch('法律詳細數據.csv'); | |
const detailText = await detailResponse.text(); | |
const parsedDetail = Papa.parse(detailText, { | |
header: true, | |
dynamicTyping: true, | |
skipEmptyLines: true | |
}); | |
laws = parsedBasic.data; | |
billsData = mergeBillsWithProgress(parsedDetail.data); | |
performSearch(); | |
// 隱藏載入畫面,顯示主要內容 | |
document.getElementById('loadingScreen').classList.add('hidden'); | |
document.getElementById('mainContent').classList.remove('hidden'); | |
// 重新創建圖標 | |
lucide.createIcons(); | |
} catch (error) { | |
console.error('載入資料時發生錯誤:', error); | |
document.getElementById('loadingScreen').innerHTML = ` | |
<div class="text-center"> | |
<p class="text-red-600">載入資料失敗</p> | |
</div> | |
`; | |
} | |
} | |
// 合併 bills 和 progress 資料 | |
function mergeBillsWithProgress(detailData) { | |
const bills = detailData.filter(item => item['資料類型'] === 'bills'); | |
const progress = detailData.filter(item => item['資料類型'] === 'progress'); | |
const progressMap = {}; | |
progress.forEach(p => { | |
const billNumber = p['關係文書/議案編號']; | |
if (billNumber && billNumber !== 'nan') { | |
progressMap[billNumber] = p; | |
} | |
}); | |
const mergedBills = bills.map(bill => { | |
const billNumber = bill['關係文書/議案編號']; | |
const relatedProgress = progressMap[billNumber]; | |
if (relatedProgress) { | |
return { | |
...bill, | |
'進度': relatedProgress['進度'] !== 'nan' ? relatedProgress['進度'] : bill['進度'], | |
'關係文書類型': relatedProgress['關係文書類型'] !== 'nan' ? relatedProgress['關係文書類型'] : bill['關係文書類型'] | |
}; | |
} | |
return bill; | |
}); | |
return mergedBills; | |
} | |
// 處理值 | |
function processValue(value) { | |
if (value === null || value === undefined || value === 'nan' || value === 'NaN') { | |
return null; | |
} | |
if (value === '' || value === ' ') { | |
return '(無資料)'; | |
} | |
return value; | |
} | |
// 格式化日期 | |
function formatDate(dateStr) { | |
if (!dateStr || dateStr === 'nan') return null; | |
try { | |
const date = new Date(dateStr); | |
return date.toLocaleDateString('zh-TW', { | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric' | |
}); | |
} catch { | |
return dateStr; | |
} | |
} | |
// 根據法律名稱獲取防中關鍵字 | |
function getAntiChinaKeywords(lawName) { | |
if (lawName.includes('兩岸人民關係條例') || lawName.includes('臺灣地區與大陸地區人民關係條例') || | |
lawName.includes('台灣地區與大陸地區人民關係條例') || lawName.includes('兩岸條例')) { | |
return ['廣告', '國家核心關鍵技術', '陸資', '中資', '共諜']; | |
} else if (lawName.includes('刑法')) { | |
return []; | |
} else { | |
return ['中國', '大陸', '境外勢力', '滲透', '統戰', '境外敵對勢力', '中共', | |
'共諜', '紅媒', '親中', '中資', '陸資']; | |
} | |
} | |
// 切換使用說明 | |
function toggleGuide() { | |
showGuide = !showGuide; | |
const container = document.getElementById('guideContainer'); | |
const content = document.getElementById('guideContent'); | |
const toggleText = document.getElementById('guideToggleText'); | |
if (showGuide) { | |
container.classList.remove('collapsed'); | |
content.style.display = 'grid'; | |
toggleText.textContent = '收起'; | |
} else { | |
container.classList.add('collapsed'); | |
content.style.display = 'none'; | |
toggleText.textContent = '展開'; | |
} | |
} | |
// 處理搜尋輸入 | |
function handleSearchInput(event) { | |
if (event.key === 'Enter') { | |
performSearch(); | |
} else { | |
performSearch(); | |
} | |
} | |
// 執行搜尋 | |
function performSearch() { | |
const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | |
const searchField = document.getElementById('searchField').value; | |
if (!searchTerm) { | |
searchResults = { laws: laws, bills: [] }; | |
} else { | |
// 過濾法律 | |
const filteredLaws = laws.filter(law => { | |
const relatedBills = billsData.filter(bill => | |
bill['法律編號'] === law['法律編號'] && bill['資料類型'] === 'bills' | |
); | |
switch (searchField) { | |
case 'all': | |
const lawMatch = ( | |
(law['法律編號'] && law['法律編號'].toString().includes(searchTerm)) || | |
(law['法律名稱'] && law['法律名稱'].toLowerCase().includes(searchTerm)) || | |
(law['別名'] && law['別名'].toLowerCase().includes(searchTerm)) || | |
(law['其他名稱'] && law['其他名稱'].toLowerCase().includes(searchTerm)) || | |
(law['主管機關'] && law['主管機關'].toLowerCase().includes(searchTerm)) | |
); | |
const billMatch = relatedBills.some(bill => ( | |
(bill['關係文書/議案編號'] && bill['關係文書/議案編號'].toString().toLowerCase().includes(searchTerm)) || | |
(bill['議案名稱'] && bill['議案名稱'].toLowerCase().includes(searchTerm)) || | |
(bill['提案人'] && bill['提案人'] !== 'nan' && bill['提案人'].toLowerCase().includes(searchTerm)) || | |
(bill['連署人'] && bill['連署人'] !== 'nan' && bill['連署人'].toLowerCase().includes(searchTerm)) || | |
(bill['提案單位/提案委員'] && bill['提案單位/提案委員'].toLowerCase().includes(searchTerm)) || | |
(bill['說明'] && bill['說明'] !== 'nan' && bill['說明'].toLowerCase().includes(searchTerm)) | |
)); | |
return lawMatch || billMatch; | |
case 'name': | |
return law['法律名稱'] && law['法律名稱'].toLowerCase().includes(searchTerm); | |
case 'billName': | |
return relatedBills.some(bill => | |
bill['議案名稱'] && bill['議案名稱'].toLowerCase().includes(searchTerm) | |
); | |
case 'proposer': | |
return relatedBills.some(bill => | |
(bill['提案人'] && bill['提案人'] !== 'nan' && bill['提案人'].toLowerCase().includes(searchTerm)) || | |
(bill['連署人'] && bill['連署人'] !== 'nan' && bill['連署人'].toLowerCase().includes(searchTerm)) || | |
(bill['提案單位/提案委員'] && bill['提案單位/提案委員'].toLowerCase().includes(searchTerm)) | |
); | |
case 'billNumber': | |
return relatedBills.some(bill => | |
bill['關係文書/議案編號'] && bill['關係文書/議案編號'].toString().toLowerCase().includes(searchTerm) | |
); | |
case 'description': | |
return relatedBills.some(bill => | |
bill['說明'] && bill['說明'] !== 'nan' && bill['說明'].toLowerCase().includes(searchTerm) | |
); | |
default: | |
return true; | |
} | |
}); | |
// 過濾議案 | |
const filteredBills = billsData.filter(bill => { | |
if (bill['資料類型'] !== 'bills') return false; | |
switch (searchField) { | |
case 'all': | |
return ( | |
(bill['關係文書/議案編號'] && bill['關係文書/議案編號'].toString().toLowerCase().includes(searchTerm)) || | |
(bill['議案名稱'] && bill['議案名稱'].toLowerCase().includes(searchTerm)) || | |
(bill['提案人'] && bill['提案人'] !== 'nan' && bill['提案人'].toLowerCase().includes(searchTerm)) || | |
(bill['連署人'] && bill['連署人'] !== 'nan' && bill['連署人'].toLowerCase().includes(searchTerm)) || | |
(bill['提案單位/提案委員'] && bill['提案單位/提案委員'].toLowerCase().includes(searchTerm)) || | |
(bill['說明'] && bill['說明'] !== 'nan' && bill['說明'].toLowerCase().includes(searchTerm)) | |
); | |
case 'billName': | |
return bill['議案名稱'] && bill['議案名稱'].toLowerCase().includes(searchTerm); | |
case 'proposer': | |
return ( | |
(bill['提案人'] && bill['提案人'] !== 'nan' && bill['提案人'].toLowerCase().includes(searchTerm)) || | |
(bill['連署人'] && bill['連署人'] !== 'nan' && bill['連署人'].toLowerCase().includes(searchTerm)) || | |
(bill['提案單位/提案委員'] && bill['提案單位/提案委員'].toLowerCase().includes(searchTerm)) | |
); | |
case 'billNumber': | |
return bill['關係文書/議案編號'] && bill['關係文書/議案編號'].toString().toLowerCase().includes(searchTerm); | |
case 'description': | |
return bill['說明'] && bill['說明'] !== 'nan' && bill['說明'].toLowerCase().includes(searchTerm); | |
default: | |
return false; | |
} | |
}); | |
searchResults = { laws: filteredLaws, bills: filteredBills }; | |
} | |
displayLaws(); | |
displayBillSearchResults(); | |
updateSearchStats(); | |
} | |
// 顯示法律列表 | |
function displayLaws() { | |
const lawsWithStats = searchResults.laws.map(law => { | |
const relatedBills = billsData.filter(bill => | |
bill['法律編號'] === law['法律編號'] && bill['資料類型'] === 'bills' | |
); | |
// 判斷法律類別 | |
let category = '國安相關'; | |
const lawName = law['法律名稱']; | |
if (lawName.includes('廣播') || lawName.includes('電視') || lawName.includes('衛星') || | |
lawName.includes('電信') || lawName.includes('通訊') || lawName.includes('媒體')) { | |
category = '媒體相關'; | |
} | |
// 防中議案統計 | |
let antiChinaKeywords = []; | |
let hasAntiChinaBills = false; | |
let antiChinaBillCount = 0; | |
if (lawName.includes('兩岸人民關係條例') || lawName.includes('臺灣地區與大陸地區人民關係條例') || | |
lawName.includes('台灣地區與大陸地區人民關係條例') || lawName.includes('兩岸條例')) { | |
antiChinaKeywords = ['廣告', '國家核心關鍵技術', '陸資', '中資', '共諜']; | |
hasAntiChinaBills = relatedBills.some(bill => { | |
const checkText = `${bill['議案名稱'] || ''} ${bill['說明'] || ''}`.toLowerCase(); | |
return antiChinaKeywords.some(keyword => checkText.includes(keyword.toLowerCase())); | |
}); | |
antiChinaBillCount = relatedBills.filter(bill => { | |
const checkText = `${bill['議案名稱'] || ''} ${bill['說明'] || ''}`.toLowerCase(); | |
return antiChinaKeywords.some(keyword => checkText.includes(keyword.toLowerCase())); | |
}).length; | |
} else if (lawName.includes('刑法')) { | |
const antiChinaBillNumbers = ['1031208070200900', '1040916070201100', '1051024070200800', | |
'1061206070200100', '1080429070200600', '1091111070200600']; | |
hasAntiChinaBills = relatedBills.some(bill => | |
antiChinaBillNumbers.includes(bill['關係文書/議案編號']?.toString()) | |
); | |
antiChinaBillCount = relatedBills.filter(bill => | |
antiChinaBillNumbers.includes(bill['關係文書/議案編號']?.toString()) | |
).length; | |
} else { | |
antiChinaKeywords = ['中國', '大陸', '境外勢力', '滲透', '統戰', '境外敵對勢力', '中共', | |
'共諜', '紅媒', '親中', '中資', '陸資']; | |
hasAntiChinaBills = relatedBills.some(bill => { | |
const checkText = `${bill['議案名稱'] || ''} ${bill['說明'] || ''}`.toLowerCase(); | |
return antiChinaKeywords.some(keyword => checkText.includes(keyword.toLowerCase())); | |
}); | |
antiChinaBillCount = relatedBills.filter(bill => { | |
const checkText = `${bill['議案名稱'] || ''} ${bill['說明'] || ''}`.toLowerCase(); | |
return antiChinaKeywords.some(keyword => checkText.includes(keyword.toLowerCase())); | |
}).length; | |
} | |
return { | |
...law, | |
billCount: relatedBills.length, | |
category: category, | |
hasAntiChinaBills: hasAntiChinaBills, | |
antiChinaBillCount: antiChinaBillCount | |
}; | |
}); | |
// 分類法律 | |
const categorizedLaws = { | |
passed: { | |
media: lawsWithStats.filter(law => law['法律狀態'] === '現行' && law.category === '媒體相關'), | |
security: lawsWithStats.filter(law => law['法律狀態'] === '現行' && law.category === '國安相關') | |
}, | |
pending: lawsWithStats.filter(law => law['法律狀態'] !== '現行') | |
}; | |
// 更新計數 | |
document.getElementById('countAll').textContent = lawsWithStats.length; | |
document.getElementById('countPassedMedia').textContent = categorizedLaws.passed.media.length; | |
document.getElementById('countPassedSecurity').textContent = categorizedLaws.passed.security.length; | |
document.getElementById('countPending').textContent = categorizedLaws.pending.length; | |
// 根據分類顯示 | |
let displayLaws = lawsWithStats; | |
switch(activeCategory) { | |
case 'passed-media': | |
displayLaws = categorizedLaws.passed.media; | |
break; | |
case 'passed-security': | |
displayLaws = categorizedLaws.passed.security; | |
break; | |
case 'pending': | |
displayLaws = categorizedLaws.pending; | |
break; | |
default: | |
displayLaws = lawsWithStats; | |
} | |
// 生成法律卡片 | |
const container = document.getElementById('lawCardsList'); | |
if (displayLaws.length === 0) { | |
container.innerHTML = ` | |
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500"> | |
此分類目前沒有法律資料 | |
</div> | |
`; | |
return; | |
} | |
container.innerHTML = displayLaws.map((law, index) => ` | |
<div class="bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer" onclick="showLawDetail('${law['法律編號'] || index}')"> | |
<div class="p-6"> | |
<div class="flex items-start justify-between"> | |
<div class="flex-1"> | |
<div class="flex items-center gap-3"> | |
<i data-lucide="file-text" class="h-5 w-5 text-blue-600 flex-shrink-0"></i> | |
<h3 class="text-xl font-semibold text-gray-900"> | |
${law['法律名稱']} | |
</h3> | |
${law['別名'] && processValue(law['別名']) ? | |
`<span class="text-sm text-gray-500"> | |
(${processValue(law['別名'])}) | |
</span>` : '' | |
} | |
<span class="ml-2 px-2 py-1 rounded-full text-xs ${ | |
law.category === '媒體相關' | |
? 'bg-green-100 text-green-800' | |
: 'bg-blue-100 text-blue-800' | |
}"> | |
${law.category} | |
</span> | |
${law.hasAntiChinaBills ? ` | |
<button | |
onclick="event.stopPropagation(); showLawDetailWithAntiChina('${law['法律編號']}')" | |
class="ml-2 px-2 py-1 rounded-full text-xs bg-red-600 text-white flex items-center gap-1 hover:bg-red-700 transition-colors" | |
title="點擊查看防中議案" | |
> | |
<i data-lucide="alert-triangle" class="h-3 w-3"></i> | |
防中議案 (${law.antiChinaBillCount}) | |
</button> | |
` : ''} | |
</div> | |
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3"> | |
<div class="flex items-center text-sm text-gray-600"> | |
<span class="font-medium mr-2">法律編號:</span> | |
${law['法律編號']} | |
</div> | |
<div class="flex items-center text-sm text-gray-600"> | |
<span class="font-medium mr-2">類別:</span> | |
${processValue(law['類別']) || '未分類'} | |
</div> | |
<div class="flex items-center text-sm text-gray-600"> | |
<span class="font-medium mr-2">狀態:</span> | |
<span class="px-2 py-1 rounded-full text-xs ${ | |
law['法律狀態'] === '現行' | |
? 'bg-green-100 text-green-800' | |
: 'bg-gray-100 text-gray-800' | |
}"> | |
${law['法律狀態']} | |
</span> | |
</div> | |
<div class="flex items-center text-sm text-gray-600"> | |
<span class="font-medium mr-2">主管機關:</span> | |
${processValue(law['主管機關']) || '未指定'} | |
</div> | |
</div> | |
<div class="mt-3 flex items-center justify-between"> | |
<div class="text-sm text-gray-500"> | |
<div>最新版本:${formatDate(law['最新版本日期'])} ${law['最新版本動作']}</div> | |
${law.billCount > 0 ? ` | |
<div class="mt-1"> | |
<span class="inline-flex items-center"> | |
<i data-lucide="file-text" class="h-3 w-3 mr-1"></i> | |
相關議案 ${law.billCount} 件 | |
</span> | |
</div> | |
` : ''} | |
</div> | |
<i data-lucide="chevron-right" class="h-5 w-5 text-gray-400"></i> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
`).join(''); | |
// 重新創建圖標 | |
lucide.createIcons(); | |
} | |
// 顯示議案搜尋結果 | |
function displayBillSearchResults() { | |
const searchTerm = document.getElementById('searchInput').value; | |
const container = document.getElementById('billSearchResults'); | |
const listContainer = document.getElementById('billSearchResultsList'); | |
if (!searchTerm || searchResults.bills.length === 0) { | |
container.classList.add('hidden'); | |
return; | |
} | |
container.classList.remove('hidden'); | |
const billsToShow = searchResults.bills.slice(0, 5); | |
listContainer.innerHTML = billsToShow.map((bill, index) => { | |
const relatedLaw = laws.find(law => law['法律編號'] === bill['法律編號']); | |
const lawName = relatedLaw?.['法律名稱'] || ''; | |
let isAntiChina = false; | |
if (lawName.includes('刑法')) { | |
const antiChinaBillNumbers = ['1031208070200900', '1040916070201100', '1051024070200800', | |
'1061206070200100', '1080429070200600', '1091111070200600']; | |
isAntiChina = antiChinaBillNumbers.includes(bill['關係文書/議案編號']?.toString()); | |
} else { | |
const antiChinaKeywords = getAntiChinaKeywords(lawName); | |
const checkText = `${bill['議案名稱'] || ''} ${bill['說明'] || ''}`.toLowerCase(); | |
isAntiChina = antiChinaKeywords.some(keyword => checkText.includes(keyword.toLowerCase())); | |
} | |
return ` | |
<div class="bg-white rounded-lg shadow p-4"> | |
<div class="flex items-start justify-between"> | |
<div class="flex-1"> | |
<h3 class="font-semibold text-gray-900 flex items-center"> | |
<button | |
onclick="showLawDetailWithBill('${bill['法律編號']}', '${bill['關係文書/議案編號']}')" | |
class="hover:text-blue-600 text-left" | |
> | |
${bill['議案名稱']} | |
</button> | |
${isAntiChina ? ` | |
<span class="ml-2 px-2 py-1 rounded-full text-xs bg-red-600 text-white flex items-center gap-1"> | |
<i data-lucide="alert-triangle" class="h-3 w-3"></i> | |
防中議案 | |
</span> | |
` : ''} | |
</h3> | |
<div class="mt-2 text-sm text-gray-600"> | |
<div>議案編號:${bill['關係文書/議案編號']}</div> | |
<div>相關法律: | |
<button | |
onclick="event.stopPropagation(); showLawDetail('${relatedLaw?.['法律編號']}')" | |
class="text-blue-600 hover:text-blue-800" | |
> | |
${relatedLaw?.['法律名稱']} | |
</button> | |
</div> | |
<div>提案人:${bill['提案單位/提案委員'] || '-'}</div> | |
<div>狀態:${bill['提案/議案最新狀態'] || '-'}</div> | |
</div> | |
${bill['說明'] && bill['說明'] !== 'nan' ? ` | |
<div class="mt-2 text-sm text-gray-500"> | |
${bill['說明'].length > 100 ? | |
bill['說明'].substring(0, 100) + '...' : | |
bill['說明'] | |
} | |
</div> | |
` : ''} | |
</div> | |
<button | |
onclick="showLawDetailWithBill('${bill['法律編號']}', '${bill['關係文書/議案編號']}')" | |
class="ml-4 text-blue-600 hover:text-blue-800 text-sm" | |
> | |
查看詳細 → | |
</button> | |
</div> | |
</div> | |
`; | |
}).join(''); | |
if (searchResults.bills.length > 5) { | |
listContainer.innerHTML += ` | |
<div class="text-center text-sm text-gray-500"> | |
還有 ${searchResults.bills.length - 5} 筆議案... | |
</div> | |
`; | |
} | |
lucide.createIcons(); | |
} | |
// 更新搜尋統計 | |
function updateSearchStats() { | |
const searchTerm = document.getElementById('searchInput').value; | |
const statsElement = document.getElementById('searchStats'); | |
if (searchTerm) { | |
let html = `<div>找到 ${searchResults.laws.length} 筆法律</div>`; | |
if (searchResults.bills.length > 0) { | |
html += `<div>找到 ${searchResults.bills.length} 筆相關議案</div>`; | |
} | |
statsElement.innerHTML = html; | |
} else { | |
statsElement.innerHTML = `<div>共 ${searchResults.laws.length} 筆法律資料</div>`; | |
} | |
} | |
// 設定分類 | |
function setActiveCategory(category) { | |
activeCategory = category; | |
// 更新按鈕樣式 | |
const buttons = { | |
'all': document.getElementById('cat-all'), | |
'passed-media': document.getElementById('cat-passed-media'), | |
'passed-security': document.getElementById('cat-passed-security'), | |
'pending': document.getElementById('cat-pending') | |
}; | |
Object.keys(buttons).forEach(key => { | |
if (key === category) { | |
buttons[key].className = 'px-4 py-2 rounded-full text-sm font-medium transition-colors ' + | |
(key === 'all' ? 'bg-blue-600 text-white' : | |
key === 'passed-media' ? 'bg-green-600 text-white' : | |
key === 'passed-security' ? 'bg-red-600 text-white' : | |
'bg-yellow-600 text-white'); | |
} else { | |
buttons[key].className = 'px-4 py-2 rounded-full text-sm font-medium transition-colors ' + | |
(key === 'all' ? 'bg-gray-100 text-gray-700 hover:bg-gray-200' : | |
key === 'passed-media' ? 'bg-green-100 text-green-700 hover:bg-green-200' : | |
key === 'passed-security' ? 'bg-red-100 text-red-700 hover:bg-red-200' : | |
'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'); | |
} | |
}); | |
displayLaws(); | |
} | |
// 顯示法律詳細資訊 | |
async function showLawDetail(lawId, autoShowAntiChina = false, targetBillNumber = null) { | |
selectedLaw = laws.find(l => l['法律編號'] == lawId); | |
if (!selectedLaw) return; | |
showOnlyAntiChina = autoShowAntiChina; | |
activeTab = targetBillNumber ? 'bills' : 'timeline'; | |
// 載入詳細資料 | |
await loadLawDetails(); | |
// 切換頁面 | |
document.getElementById('lawListPage').classList.add('hidden'); | |
document.getElementById('lawDetailPage').classList.remove('hidden'); | |
// 生成詳細頁面內容 | |
renderLawDetailPage(); | |
// 如果有目標議案,滾動到該議案 | |
if (targetBillNumber) { | |
setTimeout(() => { | |
const targetElement = document.querySelector(`[data-bill-number="${targetBillNumber}"]`); | |
if (targetElement) { | |
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
// 展開該議案 | |
const expandButton = targetElement.querySelector('[data-expand-bill]'); | |
if (expandButton) { | |
expandButton.click(); | |
} | |
} | |
}, 500); | |
} | |
} | |
// 顯示法律詳細資訊(含防中篩選) | |
function showLawDetailWithAntiChina(lawId) { | |
showLawDetail(lawId, true); | |
} | |
// 顯示法律詳細資訊(含議案跳轉) | |
function showLawDetailWithBill(lawId, billNumber) { | |
showLawDetail(lawId, false, billNumber); | |
} | |
// 載入法律詳細資料 | |
async function loadLawDetails() { | |
try { | |
// 載入版本資料 | |
const versionResponse = await fetch('法律版本.csv'); | |
const versionText = await versionResponse.text(); | |
const parsedVersion = Papa.parse(versionText, { | |
header: true, | |
dynamicTyping: true, | |
skipEmptyLines: true | |
}); | |
versions = parsedVersion.data.filter(v => v['法律編號'] === selectedLaw['法律編號']); | |
// 篩選議案 | |
const lawBills = billsData.filter(b => b['法律編號'] === selectedLaw['法律編號'] && b['資料類型'] === 'bills'); | |
// 加上防中標記 | |
bills = lawBills.map(bill => ({ | |
...bill, | |
isAntiChina: isAntiChinaRelated( | |
`${bill['議案名稱'] || ''} ${bill['說明'] || ''}`, | |
bill['關係文書/議案編號'] | |
) | |
})); | |
// 載入進度資料 | |
try { | |
const progressResponse = await fetch('paste.txt'); | |
const progressText = await progressResponse.text(); | |
progressData = JSON.parse(progressText); | |
} catch (e) { | |
progressData = []; | |
} | |
} catch (error) { | |
console.error('載入詳細資料時發生錯誤:', error); | |
} | |
} | |
// 檢查是否為防中相關 | |
function isAntiChinaRelated(text, billNumber = null) { | |
if (!text) return false; | |
if (selectedLaw['法律名稱'].includes('刑法')) { | |
const antiChinaBillNumbers = ['1031208070200900', '1040916070201100', '1051024070200800', | |
'1061206070200100', '1080429070200600', '1091111070200600']; | |
return billNumber && antiChinaBillNumbers.includes(billNumber.toString()); | |
} | |
const antiChinaKeywords = getAntiChinaKeywords(selectedLaw['法律名稱']); | |
const lowerText = text.toLowerCase(); | |
return antiChinaKeywords.some(keyword => lowerText.includes(keyword.toLowerCase())); | |
} | |
// 渲染法律詳細頁面 | |
function renderLawDetailPage() { | |
const antiChinaBillCount = bills.filter(bill => bill.isAntiChina).length; | |
const antiChinaKeywords = getAntiChinaKeywords(selectedLaw['法律名稱']); | |
const detailPage = document.getElementById('lawDetailPage'); | |
detailPage.innerHTML = ` | |
<div class="min-h-screen bg-gray-50"> | |
<header class="bg-white shadow-sm border-b"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
<button | |
onclick="backToList()" | |
class="mb-4 text-blue-600 hover:text-blue-800 font-medium flex items-center" | |
> | |
<i data-lucide="chevron-right" class="h-4 w-4 rotate-180 mr-1"></i> | |
返回列表 | |
</button> | |
<div class="flex items-start justify-between"> | |
<div> | |
<h1 class="text-3xl font-bold text-gray-900">${selectedLaw['法律名稱']}</h1> | |
${selectedLaw['別名'] && processValue(selectedLaw['別名']) ? | |
`<p class="mt-1 text-gray-600">別名:${processValue(selectedLaw['別名'])}</p>` : '' | |
} | |
<div class="mt-2 flex items-center gap-4 text-sm text-gray-600"> | |
<span>法律編號:${selectedLaw['法律編號']}</span> | |
<span>主管機關:${processValue(selectedLaw['主管機關']) || '未指定'}</span> | |
<span class="px-2 py-1 rounded-full text-xs ${ | |
selectedLaw['法律狀態'] === '現行' | |
? 'bg-green-100 text-green-800' | |
: 'bg-gray-100 text-gray-800' | |
}"> | |
${selectedLaw['法律狀態']} | |
</span> | |
</div> | |
</div> | |
${antiChinaBillCount > 0 ? ` | |
<div class="flex flex-col items-end gap-2"> | |
<button | |
onclick="toggleAntiChinaFilter()" | |
class="px-4 py-2 rounded-full text-sm font-medium transition-colors flex items-center gap-2 ${ | |
showOnlyAntiChina | |
? 'bg-red-600 text-white' | |
: 'bg-red-100 text-red-700 hover:bg-red-200' | |
}" | |
> | |
<i data-lucide="shield" class="h-4 w-4"></i> | |
防中議案 (${antiChinaBillCount}) | |
${showOnlyAntiChina ? ' ✓' : ''} | |
</button> | |
${antiChinaKeywords.length > 0 ? ` | |
<div class="text-xs text-gray-500 text-right max-w-xs"> | |
關鍵字:${antiChinaKeywords.join('、')} | |
</div> | |
` : ''} | |
</div> | |
` : ''} | |
</div> | |
</div> | |
</header> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
<!-- Tab 切換 --> | |
<div class="bg-white rounded-lg shadow mb-6"> | |
<div class="border-b"> | |
<nav class="flex -mb-px"> | |
<button | |
onclick="setActiveTab('timeline')" | |
id="tab-timeline" | |
class="py-3 px-6 border-b-2 font-medium text-sm ${ | |
activeTab === 'timeline' | |
? 'border-blue-500 text-blue-600' | |
: 'border-transparent text-gray-500 hover:text-gray-700' | |
}" | |
> | |
<i data-lucide="calendar" class="inline-block h-4 w-4 mr-2"></i> | |
歷程時間軸 | |
</button> | |
<button | |
onclick="setActiveTab('versions')" | |
id="tab-versions" | |
class="py-3 px-6 border-b-2 font-medium text-sm ${ | |
activeTab === 'versions' | |
? 'border-blue-500 text-blue-600' | |
: 'border-transparent text-gray-500 hover:text-gray-700' | |
}" | |
> | |
<i data-lucide="file-text" class="inline-block h-4 w-4 mr-2"></i> | |
版本列表 | |
</button> | |
<button | |
onclick="setActiveTab('bills')" | |
id="tab-bills" | |
class="py-3 px-6 border-b-2 font-medium text-sm ${ | |
activeTab === 'bills' | |
? 'border-blue-500 text-blue-600' | |
: 'border-transparent text-gray-500 hover:text-gray-700' | |
}" | |
> | |
<i data-lucide="users" class="inline-block h-4 w-4 mr-2"></i> | |
相關議案 (<span id="billsCount">0</span>) | |
</button> | |
</nav> | |
</div> | |
<!-- 法律內容搜尋 --> | |
<div class="p-4 bg-gray-50"> | |
<div class="relative"> | |
<i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4"></i> | |
<input | |
type="text" | |
id="detailSearchInput" | |
placeholder="在${selectedLaw['法律名稱']}的議案中搜尋..." | |
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
onkeyup="performDetailSearch()" | |
/> | |
</div> | |
<div id="detailSearchStats" class="mt-2 text-xs text-gray-600"></div> | |
</div> | |
</div> | |
<!-- 內容區域 --> | |
<div id="tabContent"></div> | |
</div> | |
</div> | |
`; | |
renderTabContent(); | |
lucide.createIcons(); | |
} | |
// 返回列表 | |
function backToList() { | |
document.getElementById('lawListPage').classList.remove('hidden'); | |
document.getElementById('lawDetailPage').classList.add('hidden'); | |
selectedLaw = null; | |
showOnlyAntiChina = false; | |
detailSearchTerm = ''; | |
} | |
// 切換防中篩選 | |
function toggleAntiChinaFilter() { | |
showOnlyAntiChina = !showOnlyAntiChina; | |
renderLawDetailPage(); | |
} | |
// 設定Tab | |
function setActiveTab(tab) { | |
activeTab = tab; | |
// 更新Tab樣式 | |
['timeline', 'versions', 'bills'].forEach(t => { | |
const tabElement = document.getElementById(`tab-${t}`); | |
if (t === tab) { | |
tabElement.className = 'py-3 px-6 border-b-2 font-medium text-sm border-blue-500 text-blue-600'; | |
} else { | |
tabElement.className = 'py-3 px-6 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700'; | |
} | |
}); | |
renderTabContent(); | |
} | |
// 詳細搜尋 | |
function performDetailSearch() { | |
detailSearchTerm = document.getElementById('detailSearchInput').value.toLowerCase(); | |
renderTabContent(); | |
} | |
// 渲染Tab內容 | |
function renderTabContent() { | |
const container = document.getElementById('tabContent'); | |
// 過濾議案 | |
let displayBills = showOnlyAntiChina ? bills.filter(bill => bill.isAntiChina) : bills; | |
if (detailSearchTerm) { | |
displayBills = displayBills.filter(bill => | |
(bill['議案名稱'] && bill['議案名稱'].toLowerCase().includes(detailSearchTerm)) || | |
(bill['議案編號'] && bill['議案編號'].toString().toLowerCase().includes(detailSearchTerm)) || | |
(bill['關係文書/議案編號'] && bill['關係文書/議案編號'].toString().toLowerCase().includes(detailSearchTerm)) || | |
(bill['提案單位/提案委員'] && bill['提案單位/提案委員'].toLowerCase().includes(detailSearchTerm)) || | |
(bill['說明'] && bill['說明'].toLowerCase().includes(detailSearchTerm)) | |
); | |
} | |
// 更新議案數量 | |
document.getElementById('billsCount').textContent = displayBills.length; | |
// 更新搜尋統計 | |
if (detailSearchTerm) { | |
document.getElementById('detailSearchStats').textContent = `找到 ${displayBills.length} 筆相關議案`; | |
} else { | |
document.getElementById('detailSearchStats').textContent = ''; | |
} | |
switch(activeTab) { | |
case 'timeline': | |
container.innerHTML = renderTimelineView(displayBills); | |
break; | |
case 'versions': | |
container.innerHTML = renderVersionsView(); | |
break; | |
case 'bills': | |
container.innerHTML = renderBillsView(displayBills); | |
break; | |
} | |
lucide.createIcons(); | |
} | |
// 渲染時間軸視圖 | |
function renderTimelineView(displayBills) { | |
const events = createTimelineEvents(displayBills); | |
if (events.length === 0) { | |
return ` | |
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500"> | |
尚無歷程資料 | |
</div> | |
`; | |
} | |
return ` | |
<div class="bg-white rounded-lg shadow p-6"> | |
<h2 class="text-xl font-semibold mb-6 flex items-center"> | |
<i data-lucide="clock" class="h-5 w-5 mr-2 text-gray-600"></i> | |
立法歷程時間軸 | |
</h2> | |
<div class="relative"> | |
<div class="timeline-line"></div> | |
<div class="space-y-6"> | |
${events.map((event, index) => renderTimelineEvent(event, index)).join('')} | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
// 建立時間軸事件 | |
function createTimelineEvents(displayBills) { | |
const events = []; | |
// 版本事件 | |
versions.forEach(version => { | |
if (version['日期'] && version['日期'] !== 'nan') { | |
events.push({ | |
date: new Date(version['日期']), | |
type: 'version', | |
title: `${version['動作']}版本`, | |
description: `版本編號:${version['版本編號']}`, | |
status: version['現行版本'] === '現行' ? '現行' : '非現行', | |
data: version | |
}); | |
} | |
}); | |
// 議案事件 | |
displayBills.forEach((bill, billIndex) => { | |
if (bill['會議/提案日期'] && bill['會議/提案日期'] !== 'nan') { | |
events.push({ | |
date: new Date(bill['會議/提案日期']), | |
type: 'bill', | |
title: '議案提出', | |
description: bill['議案名稱'], | |
billIndex: billIndex, | |
billData: bill, | |
isAntiChina: bill.isAntiChina | |
}); | |
} | |
}); | |
// 排序 | |
events.sort((a, b) => b.date - a.date); | |
return events; | |
} | |
// 渲染時間軸事件 | |
function renderTimelineEvent(event, index) { | |
const getEventIcon = () => { | |
if (event.type === 'version') { | |
return event.status === '現行' ? | |
'<i data-lucide="check-circle" class="h-5 w-5 text-green-600"></i>' : | |
'<i data-lucide="circle" class="h-5 w-5 text-gray-400"></i>'; | |
} | |
if (event.type === 'bill') { | |
return event.isAntiChina ? | |
'<i data-lucide="alert-triangle" class="h-5 w-5 text-red-600"></i>' : | |
'<i data-lucide="file-text" class="h-5 w-5 text-purple-600"></i>'; | |
} | |
return '<i data-lucide="alert-circle" class="h-5 w-5 text-blue-600"></i>'; | |
}; | |
const getEventColor = () => { | |
if (event.type === 'version') { | |
return event.status === '現行' ? 'bg-green-100 border-green-300' : 'bg-gray-100 border-gray-300'; | |
} | |
if (event.type === 'bill') { | |
return event.isAntiChina ? 'bg-red-100 border-red-300' : 'bg-purple-100 border-purple-300'; | |
} | |
return 'bg-blue-100 border-blue-300'; | |
}; | |
return ` | |
<div class="relative flex items-start"> | |
<div class="timeline-node"></div> | |
<div class="ml-16 flex-1"> | |
<div class="p-4 rounded-lg border ${getEventColor()}"> | |
<div class="flex items-start justify-between"> | |
<div class="flex-1"> | |
<div class="flex items-center"> | |
${getEventIcon()} | |
<h3 class="ml-2 font-semibold text-gray-900"> | |
${event.title} | |
${event.isAntiChina ? ` | |
<span class="ml-2 px-2 py-1 rounded-full text-xs bg-red-600 text-white"> | |
防中議案 | |
</span> | |
` : ''} | |
</h3> | |
</div> | |
<p class="mt-1 text-sm text-gray-600">${event.description}</p> | |
<p class="mt-1 text-xs text-gray-500"> | |
${event.date.toLocaleDateString('zh-TW', { | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric' | |
})} | |
</p> | |
</div> | |
${event.type === 'version' && event.data ? ` | |
<button | |
onclick="toggleTimelineDetail('version-${index}')" | |
class="ml-4 text-blue-600 hover:text-blue-800 text-sm" | |
> | |
<span id="toggle-text-version-${index}">詳細</span> | |
</button> | |
` : ''} | |
${event.type === 'bill' ? ` | |
<button | |
onclick="toggleTimelineDetail('bill-${index}')" | |
class="ml-4 text-purple-600 hover:text-purple-800 text-sm font-medium" | |
> | |
<span id="toggle-text-bill-${index}">查看議案</span> → | |
</button> | |
` : ''} | |
</div> | |
${event.type === 'version' ? ` | |
<div id="detail-version-${index}" class="hidden mt-4 pt-4 border-t border-gray-200"> | |
<div class="grid grid-cols-2 gap-3 text-sm"> | |
${event.data['委員會'] ? ` | |
<div> | |
<span class="font-medium">審查委員會:</span> | |
<span class="ml-2 text-gray-600">${event.data['委員會']}</span> | |
</div> | |
` : ''} | |
<div> | |
<span class="font-medium">歷程總數:</span> | |
<span class="ml-2 text-gray-600">${event.data['歷程總數']} 次</span> | |
</div> | |
<div> | |
<span class="font-medium">一讀:</span> | |
<span class="ml-2 text-gray-600">${event.data['一讀次數']} 次</span> | |
</div> | |
<div> | |
<span class="font-medium">二讀:</span> | |
<span class="ml-2 text-gray-600">${event.data['二讀次數']} 次</span> | |
</div> | |
<div> | |
<span class="font-medium">三讀:</span> | |
<span class="ml-2 text-gray-600">${event.data['三讀次數']} 次</span> | |
</div> | |
<div> | |
<span class="font-medium">委員會審查:</span> | |
<span class="ml-2 text-gray-600">${event.data['委員會審查次數']} 次</span> | |
</div> | |
</div> | |
</div> | |
` : ''} | |
${event.type === 'bill' ? ` | |
<div id="detail-bill-${index}" class="hidden mt-4 pt-4 border-t border-gray-200"> | |
${renderBillDetail(event.billData, event.isAntiChina)} | |
</div> | |
` : ''} | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
// 切換時間軸詳細資訊 | |
function toggleTimelineDetail(id) { | |
const detailElement = document.getElementById(`detail-${id}`); | |
const toggleText = document.getElementById(`toggle-text-${id}`); | |
if (detailElement.classList.contains('hidden')) { | |
detailElement.classList.remove('hidden'); | |
toggleText.textContent = '收起'; | |
} else { | |
detailElement.classList.add('hidden'); | |
toggleText.textContent = id.includes('version') ? '詳細' : '查看議案'; | |
} | |
} | |
// 渲染議案詳細資訊 | |
function renderBillDetail(bill, isAntiChina) { | |
const antiChinaKeywords = getAntiChinaKeywords(selectedLaw['法律名稱']); | |
let html = ` | |
<div class="grid grid-cols-2 gap-3 text-sm mb-4"> | |
<div> | |
<span class="font-medium">提案人:</span> | |
<span class="ml-2 text-gray-600">${bill['提案單位/提案委員']}</span> | |
</div> | |
<div> | |
<span class="font-medium">狀態:</span> | |
<span class="ml-2 text-gray-600">${bill['提案/議案最新狀態']}</span> | |
</div> | |
<div> | |
<span class="font-medium">類別:</span> | |
<span class="ml-2 text-gray-600">${bill['議案類別']}</span> | |
</div> | |
<div> | |
<span class="font-medium">議案編號:</span> | |
<span class="ml-2 text-gray-600">${bill['關係文書/議案編號']}</span> | |
</div> | |
</div> | |
`; | |
// 議案說明 | |
if (bill['說明'] && bill['說明'] !== 'nan') { | |
let text = bill['說明']; | |
// 高亮防中關鍵字 | |
if (isAntiChina && antiChinaKeywords.length > 0) { | |
antiChinaKeywords.forEach(keyword => { | |
const regex = new RegExp(`(${keyword})`, 'gi'); | |
text = text.replace(regex, '【$1】'); | |
}); | |
} | |
html += ` | |
<div class="bg-gray-50 rounded p-4"> | |
<h4 class="font-medium text-gray-900 mb-2">議案說明</h4> | |
<p class="text-sm text-gray-600 whitespace-pre-wrap">${text}</p> | |
</div> | |
`; | |
} | |
// 附件 | |
const attachments = []; | |
for (let i = 1; i <= 6; i++) { | |
const name = bill[`附件${i}_名稱`]; | |
const url = bill[`附件${i}_網址`]; | |
if (name && name !== 'nan' && url && url !== 'nan') { | |
attachments.push({ name, url, index: i }); | |
} | |
} | |
if (attachments.length > 0) { | |
html += ` | |
<div class="mt-4"> | |
<h4 class="font-medium text-gray-900 mb-2">相關附件</h4> | |
<div class="space-y-1"> | |
${attachments.map(attachment => ` | |
<div> | |
<a | |
href="${attachment.url}" | |
target="_blank" | |
rel="noopener noreferrer" | |
class="text-blue-600 hover:text-blue-800 text-sm" | |
> | |
${attachment.name} → | |
</a> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
`; | |
} | |
// 公報連結 | |
if (bill['公報資訊網'] && bill['公報資訊網'] !== 'nan') { | |
html += ` | |
<div class="mt-4"> | |
<a | |
href="${bill['公報資訊網']}" | |
target="_blank" | |
rel="noopener noreferrer" | |
class="text-blue-600 hover:text-blue-800 text-sm font-medium" | |
> | |
查看立法院議事暨公報資訊網 → | |
</a> | |
</div> | |
`; | |
} | |
return html; | |
} | |
// 渲染版本列表視圖 | |
function renderVersionsView() { | |
if (versions.length === 0) { | |
return ` | |
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500"> | |
尚無版本資料 | |
</div> | |
`; | |
} | |
const sortedVersions = [...versions].sort((a, b) => { | |
const dateA = new Date(a['日期']); | |
const dateB = new Date(b['日期']); | |
return dateB - dateA; | |
}); | |
return ` | |
<div class="bg-white rounded-lg shadow"> | |
<div class="p-6"> | |
<h2 class="text-xl font-semibold mb-6 flex items-center"> | |
<i data-lucide="file-text" class="h-5 w-5 mr-2 text-gray-600"></i> | |
法律版本列表 | |
</h2> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
版本日期 | |
</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
動作 | |
</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
狀態 | |
</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
委員會 | |
</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
歷程統計 | |
</th> | |
</tr> | |
</thead> | |
<tbody class="bg-white divide-y divide-gray-200"> | |
${sortedVersions.map((version, index) => ` | |
<tr class="hover:bg-gray-50"> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | |
${formatDate(version['日期'])} | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm"> | |
<span class="px-2 py-1 rounded-full text-xs ${ | |
version['動作'] === '制定' | |
? 'bg-blue-100 text-blue-800' | |
: 'bg-yellow-100 text-yellow-800' | |
}"> | |
${version['動作']} | |
</span> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm"> | |
<span class="px-2 py-1 rounded-full text-xs ${ | |
version['現行版本'] === '現行' | |
? 'bg-green-100 text-green-800' | |
: 'bg-gray-100 text-gray-800' | |
}"> | |
${version['現行版本']} | |
</span> | |
</td> | |
<td class="px-6 py-4 text-sm text-gray-900"> | |
${processValue(version['委員會']) || '-'} | |
</td> | |
<td class="px-6 py-4 text-sm text-gray-600"> | |
<div class="space-y-1"> | |
<div>總計:${version['歷程總數']} 次</div> | |
<div class="text-xs"> | |
一讀 ${version['一讀次數']} / | |
二讀 ${version['二讀次數']} / | |
三讀 ${version['三讀次數']} | |
</div> | |
</div> | |
</td> | |
</tr> | |
`).join('')} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
// 渲染議案列表視圖 | |
function renderBillsView(displayBills) { | |
if (displayBills.length === 0) { | |
return ` | |
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500"> | |
尚無相關議案資料 | |
</div> | |
`; | |
} | |
const sortedBills = [...displayBills].sort((a, b) => { | |
const dateA = a['會議/提案日期'] ? new Date(a['會議/提案日期']) : new Date(0); | |
const dateB = b['會議/提案日期'] ? new Date(b['會議/提案日期']) : new Date(0); | |
return dateB - dateA; | |
}); | |
return ` | |
<div class="space-y-4"> | |
${sortedBills.map((bill, index) => renderBillCard(bill, index)).join('')} | |
</div> | |
`; | |
} | |
// 渲染議案卡片 | |
function renderBillCard(bill, index) { | |
// 解析議案流程 | |
let billProgress = []; | |
try { | |
if (bill['議案流程'] && bill['議案流程'] !== 'nan') { | |
billProgress = JSON.parse(bill['議案流程']); | |
} | |
} catch (e) { | |
console.error('解析議案流程失敗:', e); | |
} | |
return ` | |
<div class="bg-white rounded-lg shadow overflow-hidden" data-bill-number="${bill['關係文書/議案編號']}"> | |
<div | |
class="p-6 cursor-pointer hover:bg-gray-50" | |
onclick="toggleBillDetail('bill-card-${index}')" | |
data-expand-bill | |
> | |
<div class="flex items-start justify-between"> | |
<div class="flex-1"> | |
<h3 class="text-lg font-semibold text-gray-900 mb-2 flex items-center"> | |
${bill['議案名稱']} | |
${bill.isAntiChina ? ` | |
<span class="ml-2 px-2 py-1 rounded-full text-xs bg-red-600 text-white flex items-center gap-1"> | |
<i data-lucide="alert-triangle" class="h-3 w-3"></i> | |
防中議案 | |
</span> | |
` : ''} | |
</h3> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm"> | |
<div> | |
<span class="font-medium text-gray-600">議案編號:</span> | |
<span class="ml-2">${bill['關係文書/議案編號'] || '-'}</span> | |
</div> | |
<div> | |
<span class="font-medium text-gray-600">議案類別:</span> | |
<span class="ml-2">${bill['議案類別'] || '-'}</span> | |
</div> | |
<div> | |
<span class="font-medium text-gray-600">提案日期:</span> | |
<span class="ml-2">${formatDate(bill['會議/提案日期']) || '-'}</span> | |
</div> | |
<div class="md:col-span-2"> | |
<span class="font-medium text-gray-600">最新狀態:</span> | |
<span class="ml-2 px-2 py-1 rounded-full text-xs ${ | |
bill['提案/議案最新狀態'] === '三讀' | |
? 'bg-green-100 text-green-800' | |
: bill['提案/議案最新狀態'] === '交付審查' | |
? 'bg-yellow-100 text-yellow-800' | |
: 'bg-gray-100 text-gray-800' | |
}"> | |
${bill['提案/議案最新狀態'] || '-'} | |
</span> | |
${bill['進度'] && bill['進度'] !== 'nan' ? ` | |
<span class="ml-2 text-sm text-gray-600"> | |
(進度:${bill['進度']}) | |
</span> | |
` : ''} | |
</div> | |
</div> | |
<div class="mt-3 text-sm text-gray-600"> | |
<span class="font-medium">提案人:</span> | |
<span class="ml-2">${processValue(bill['提案單位/提案委員']) || '-'}</span> | |
</div> | |
</div> | |
<i data-lucide="chevron-right" id="chevron-bill-card-${index}" | |
class="h-5 w-5 text-gray-400 transform transition-transform"></i> | |
</div> | |
</div> | |
<div id="detail-bill-card-${index}" class="hidden border-t px-6 py-4 bg-gray-50"> | |
${renderBillCardDetail(bill, billProgress)} | |
</div> | |
</div> | |
`; | |
} | |
// 切換議案詳細資訊 | |
function toggleBillDetail(id) { | |
const detailElement = document.getElementById(`detail-${id}`); | |
const chevron = document.getElementById(`chevron-${id}`); | |
if (detailElement.classList.contains('hidden')) { | |
detailElement.classList.remove('hidden'); | |
chevron.classList.add('rotate-90'); | |
} else { | |
detailElement.classList.add('hidden'); | |
chevron.classList.remove('rotate-90'); | |
} | |
} | |
// 渲染議案卡片詳細資訊 | |
function renderBillCardDetail(bill, billProgress) { | |
let html = ''; | |
// 說明 | |
if (bill['說明'] && bill['說明'] !== 'nan') { | |
html += ` | |
<div class="mb-4"> | |
<h4 class="font-medium text-gray-900 mb-2">說明</h4> | |
<p class="text-sm text-gray-600 whitespace-pre-wrap">${bill['說明']}</p> | |
</div> | |
`; | |
} | |
// 議案流程 | |
if (billProgress.length > 0) { | |
html += ` | |
<div> | |
<h4 class="font-medium text-gray-900 mb-3">議案流程</h4> | |
<div class="space-y-2"> | |
${billProgress.reverse().map((progress, pIndex) => ` | |
<div class="bg-white rounded p-3 text-sm"> | |
<div class="flex items-center justify-between"> | |
<div> | |
<span class="font-medium">${progress['狀態']}</span> | |
<span class="mx-2 text-gray-400">|</span> | |
<span class="text-gray-600">${progress['院會/委員會']}</span> | |
<span class="mx-2 text-gray-400">|</span> | |
<span class="text-gray-600">會期 ${progress['會期']}</span> | |
</div> | |
<div class="text-gray-500"> | |
${progress['日期'] ? progress['日期'].join(', ') : ''} | |
</div> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
`; | |
} | |
// 附件 | |
const attachments = []; | |
for (let i = 1; i <= 6; i++) { | |
const name = bill[`附件${i}_名稱`]; | |
const url = bill[`附件${i}_網址`]; | |
if (name && name !== 'nan' && url && url !== 'nan') { | |
attachments.push({ name, url, index: i }); | |
} | |
} | |
if (attachments.length > 0) { | |
html += ` | |
<div class="mt-4"> | |
<h4 class="font-medium text-gray-900 mb-2">相關附件</h4> | |
<div class="space-y-1"> | |
${attachments.map(attachment => ` | |
<div> | |
<a | |
href="${attachment.url}" | |
target="_blank" | |
rel="noopener noreferrer" | |
class="text-blue-600 hover:text-blue-800 text-sm" | |
> | |
${attachment.name} → | |
</a> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
`; | |
} | |
// 公報連結 | |
if (bill['公報資訊網'] && bill['公報資訊網'] !== 'nan') { | |
html += ` | |
<div class="mt-4"> | |
<a | |
href="${bill['公報資訊網']}" | |
target="_blank" | |
rel="noopener noreferrer" | |
class="text-blue-600 hover:text-blue-800 text-sm font-medium" | |
> | |
查看立法院議事暨公報資訊網 → | |
</a> | |
</div> | |
`; | |
} | |
return html; | |
} | |
</script> | |
</body> | |
</html> |