// 全域變數 let currentPage = 1; let currentFilters = {}; // 初始化 document.addEventListener('DOMContentLoaded', function() { try { loadStatistics(); loadCategories(); loadArticles(); loadCrawlerConfig(); // 綁定事件 const searchBtn = document.getElementById('searchBtn'); const resetBtn = document.getElementById('resetBtn'); const refreshBtn = document.getElementById('refreshBtn'); const runCrawlerBtn = document.getElementById('runCrawlerBtn'); const closeBtn = document.querySelector('.close'); if (searchBtn) searchBtn.addEventListener('click', handleSearch); if (resetBtn) resetBtn.addEventListener('click', handleReset); if (refreshBtn) { refreshBtn.addEventListener('click', function() { loadStatistics(); loadArticles(); }); } if (runCrawlerBtn) runCrawlerBtn.addEventListener('click', handleRunCrawler); // 模態框關閉 if (closeBtn) { closeBtn.addEventListener('click', function() { const modal = document.getElementById('articleModal'); if (modal) modal.style.display = 'none'; }); } window.addEventListener('click', function(event) { const modal = document.getElementById('articleModal'); if (event.target === modal && modal) { modal.style.display = 'none'; } }); // 頁籤切換 document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchTab(tabName); }); }); // 爬蟲設定相關事件 const addUrlBtn = document.getElementById('addUrlBtn'); const saveConfigBtn = document.getElementById('saveConfigBtn'); const loadConfigBtn = document.getElementById('loadConfigBtn'); const resetConfigBtn = document.getElementById('resetConfigBtn'); const testConfigBtn = document.getElementById('testConfigBtn'); if (addUrlBtn) addUrlBtn.addEventListener('click', addUrlItem); if (saveConfigBtn) saveConfigBtn.addEventListener('click', saveCrawlerConfig); if (loadConfigBtn) loadConfigBtn.addEventListener('click', loadCrawlerConfig); if (resetConfigBtn) resetConfigBtn.addEventListener('click', resetCrawlerConfig); if (testConfigBtn) testConfigBtn.addEventListener('click', testCrawlerConfig); } catch (error) { console.error('初始化失敗:', error); } }); // 載入統計資料 async function loadStatistics() { try { const response = await fetch('/api/statistics'); const result = await response.json(); if (result.success) { const stats = result.data; document.getElementById('totalArticles').textContent = stats.total_articles || 0; document.getElementById('paywalledArticles').textContent = stats.paywall[1] || 0; document.getElementById('freeArticles').textContent = stats.paywall[0] || 0; document.getElementById('categoryCount').textContent = stats.categories.length || 0; // 繪製圖表 drawCategoryChart(stats.categories); drawAuthorChart(stats.authors); } } catch (error) { console.error('載入統計資料失敗:', error); } } // 載入分類列表 async function loadCategories() { try { const response = await fetch('/api/categories'); const result = await response.json(); if (result.success) { const categorySelect = document.getElementById('category'); result.data.forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); } } catch (error) { console.error('載入分類列表失敗:', error); } } // 載入文章列表 async function loadArticles(page = 1) { currentPage = page; showLoading(true); try { const params = new URLSearchParams({ page: page, per_page: 20 }); // 加入篩選條件 Object.keys(currentFilters).forEach(key => { if (currentFilters[key]) { params.append(key, currentFilters[key]); } }); const response = await fetch(`/api/articles?${params}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('API 返回結果:', result); // 除錯用 if (result.success) { const articles = result.data || []; console.log('文章數量:', articles.length); // 除錯用 displayArticles(articles); if (result.pagination) { displayPagination(result.pagination); } } else { console.error('API 返回錯誤:', result.error); const errorMsg = result.error || '未知錯誤'; alert('載入文章失敗: ' + errorMsg); } } catch (error) { console.error('載入文章失敗:', error); alert('載入文章失敗,請稍後再試: ' + error.message); } finally { showLoading(false); } } // 顯示文章列表 function displayArticles(articles) { const list = document.getElementById('articlesList'); if (!list) { console.error('找不到 articlesList 元素'); return; } list.innerHTML = ''; if (!articles || articles.length === 0) { list.innerHTML = '
沒有找到符合條件的文章
'; return; } console.log('準備顯示', articles.length, '篇文章'); // 除錯用 articles.forEach((article, index) => { try { const card = document.createElement('div'); card.className = 'article-card'; card.onclick = () => showArticleDetail(article.id); // 處理標籤(可能是字串或空值) let tags = []; if (article.tags) { if (typeof article.tags === 'string' && article.tags.trim()) { tags = article.tags.split(',').map(t => t.trim()).filter(t => t); } else if (Array.isArray(article.tags)) { tags = article.tags.filter(t => t); } } const title = escapeHtml(article.title || '無標題'); const author = article.author ? escapeHtml(article.author) : ''; const category = article.category ? escapeHtml(article.category) : ''; const summary = article.summary ? escapeHtml(String(article.summary).substring(0, 200)) + (String(article.summary).length > 200 ? '...' : '') : ''; card.innerHTML = `
${title}
${author ? `👤 ${author}` : ''} ${article.publish_date ? `📅 ${formatDate(article.publish_date)}` : ''} ${category ? `📁 ${category}` : ''} ${article.is_paywalled ? '💰 付費文章' : ''}
${summary ? `
${summary}
` : ''} ${tags.length > 0 ? `
${tags.map(t => `${escapeHtml(t)}`).join('')}
` : ''} `; list.appendChild(card); } catch (error) { console.error(`處理第 ${index + 1} 篇文章時發生錯誤:`, error, article); } }); console.log('文章列表顯示完成'); // 除錯用 } // 顯示分頁 function displayPagination(pagination) { const paginationDiv = document.getElementById('pagination'); const infoDiv = document.getElementById('paginationInfo'); infoDiv.textContent = `第 ${pagination.page} 頁,共 ${pagination.pages} 頁,總計 ${pagination.total} 篇文章`; paginationDiv.innerHTML = ''; // 上一頁 const prevBtn = document.createElement('button'); prevBtn.textContent = '← 上一頁'; prevBtn.disabled = pagination.page === 1; prevBtn.onclick = () => loadArticles(pagination.page - 1); paginationDiv.appendChild(prevBtn); // 頁碼 const startPage = Math.max(1, pagination.page - 2); const endPage = Math.min(pagination.pages, pagination.page + 2); for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement('button'); pageBtn.textContent = i; pageBtn.className = i === pagination.page ? 'active' : ''; pageBtn.onclick = () => loadArticles(i); paginationDiv.appendChild(pageBtn); } // 下一頁 const nextBtn = document.createElement('button'); nextBtn.textContent = '下一頁 →'; nextBtn.disabled = pagination.page === pagination.pages; nextBtn.onclick = () => loadArticles(pagination.page + 1); paginationDiv.appendChild(nextBtn); } // 顯示文章詳情 async function showArticleDetail(articleId) { try { const response = await fetch(`/api/article/${articleId}`); const result = await response.json(); if (result.success) { const article = result.data; const detailDiv = document.getElementById('articleDetail'); const tags = article.tags ? article.tags.split(',').map(t => t.trim()).filter(t => t) : []; detailDiv.innerHTML = `

${escapeHtml(article.title || '無標題')}

${article.author ? `👤 作者: ${escapeHtml(article.author)}` : ''} ${article.publish_date ? `📅 發布日期: ${formatDate(article.publish_date)}` : ''} ${article.category ? `📁 分類: ${escapeHtml(article.category)}` : ''} ${article.is_paywalled ? '💰 付費文章' : ''}
${article.summary ? `
摘要:${escapeHtml(article.summary)}
` : ''} ${tags.length > 0 ? `
標籤: ${tags.map(t => `${escapeHtml(t)}`).join('')}
` : ''} ${article.content ? `
${escapeHtml(article.content)}
` : '
此文章無內容(可能是付費文章)
'}
🔗 查看原文
`; document.getElementById('articleModal').style.display = 'block'; } } catch (error) { console.error('載入文章詳情失敗:', error); alert('載入文章詳情失敗'); } } // 處理搜尋 function handleSearch() { currentFilters = { keyword: document.getElementById('keyword').value.trim(), category: document.getElementById('category').value, tag: document.getElementById('tag').value.trim(), start_date: document.getElementById('startDate').value, end_date: document.getElementById('endDate').value, is_paywalled: document.getElementById('isPaywalled').value }; // 移除空值 Object.keys(currentFilters).forEach(key => { if (!currentFilters[key]) delete currentFilters[key]; }); loadArticles(1); } // 處理重置 function handleReset() { document.getElementById('keyword').value = ''; document.getElementById('category').value = ''; document.getElementById('tag').value = ''; document.getElementById('startDate').value = ''; document.getElementById('endDate').value = ''; document.getElementById('isPaywalled').value = ''; currentFilters = {}; loadArticles(1); } // 執行爬蟲 async function handleRunCrawler() { if (!confirm('確定要執行爬蟲嗎?這可能需要幾分鐘時間。')) { return; } const btn = document.getElementById('runCrawlerBtn'); btn.disabled = true; btn.textContent = '執行中...'; try { const response = await fetch('/api/run-crawler', { method: 'POST' }); const result = await response.json(); if (result.success) { alert('爬蟲執行成功!'); loadStatistics(); loadArticles(); } else { alert('爬蟲執行失敗: ' + result.error); } } catch (error) { console.error('執行爬蟲失敗:', error); alert('執行爬蟲失敗,請稍後再試'); } finally { btn.disabled = false; btn.textContent = '🚀 執行爬蟲'; } } // 繪製分類圖表 function drawCategoryChart(categories) { const ctx = document.getElementById('categoryChart').getContext('2d'); if (window.categoryChart && typeof window.categoryChart.destroy === 'function') { window.categoryChart.destroy(); } window.categoryChart = new Chart(ctx, { type: 'doughnut', data: { labels: categories.map(c => c.name), datasets: [{ data: categories.map(c => c.count), backgroundColor: [ '#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe', '#43e97b', '#fa709a', '#fee140', '#30cfd0', '#330867' ] }] }, options: { responsive: true, maintainAspectRatio: true } }); } // 繪製作者圖表 function drawAuthorChart(authors) { const ctx = document.getElementById('authorChart').getContext('2d'); if (window.authorChart && typeof window.authorChart.destroy === 'function') { window.authorChart.destroy(); } window.authorChart = new Chart(ctx, { type: 'bar', data: { labels: authors.map(a => a.name), datasets: [{ label: '文章數量', data: authors.map(a => a.count), backgroundColor: '#667eea' }] }, options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true } } } }); } // 工具函數 function showLoading(show) { document.getElementById('loading').classList.toggle('show', show); } function formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('zh-TW'); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ==================== 頁籤功能 ==================== function switchTab(tabName) { // 隱藏所有頁籤內容 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // 移除所有按鈕的 active 狀態 document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); // 顯示選中的頁籤 const targetTab = document.getElementById(`${tabName}-tab`); const targetBtn = document.querySelector(`[data-tab="${tabName}"]`); if (targetTab) targetTab.classList.add('active'); if (targetBtn) targetBtn.classList.add('active'); } // ==================== 爬蟲設定功能 ==================== // 預設的起始 URL const defaultUrls = [ 'https://www.hbrtaiwan.com/', 'https://www.hbrtaiwan.com/topic/management', 'https://www.hbrtaiwan.com/topic/leadership', 'https://www.hbrtaiwan.com/topic/strategy', 'https://www.hbrtaiwan.com/topic/innovation', 'https://www.hbrtaiwan.com/topic/technology' ]; // 載入爬蟲設定 async function loadCrawlerConfig() { try { // 先嘗試從伺服器載入設定 try { const response = await fetch('/api/load-crawler-config'); const result = await response.json(); if (result.success && result.data) { applyConfigToForm(result.data); // 同時儲存到 localStorage localStorage.setItem('crawlerConfig', JSON.stringify(result.data)); showConfigStatus('設定已從伺服器載入', 'success'); return; } } catch (apiError) { console.log('無法從伺服器載入設定,嘗試從本地載入:', apiError); } // 從 localStorage 載入設定(如果有的話) const savedConfig = localStorage.getItem('crawlerConfig'); if (savedConfig) { const config = JSON.parse(savedConfig); applyConfigToForm(config); showConfigStatus('設定已從本地載入', 'info'); return; } // 載入預設設定 loadDefaultConfig(); } catch (error) { console.error('載入設定失敗:', error); loadDefaultConfig(); } } // 載入預設設定 function loadDefaultConfig() { // 載入預設 URL const urlList = document.getElementById('urlList'); if (urlList) { urlList.innerHTML = ''; defaultUrls.forEach(url => { addUrlItem(url); }); } // 載入預設值 document.getElementById('downloadDelay').value = '1'; document.getElementById('maxDepth').value = '3'; document.getElementById('concurrentRequests').value = '16'; document.getElementById('skipPaywalled').checked = true; document.getElementById('followPagination').checked = true; document.getElementById('obeyRobotsTxt').checked = true; document.getElementById('articleListSelector').value = '.articleItem, article, .article-item, .post-item, .content-item'; document.getElementById('titleSelector').value = 'h1.articleTitle, h1.article-title, h1, .article-title, .post-title'; document.getElementById('authorSelector').value = '.authorName, .author, .byline, .writer, .author-name'; document.getElementById('contentSelector').value = '.articleContent, .article-content, .post-content, .content, .articleText'; } // 將設定應用到表單 function applyConfigToForm(config) { if (config.urls && Array.isArray(config.urls)) { const urlList = document.getElementById('urlList'); if (urlList) { urlList.innerHTML = ''; config.urls.forEach(url => { addUrlItem(url); }); } } if (config.downloadDelay !== undefined) { document.getElementById('downloadDelay').value = config.downloadDelay; } if (config.maxDepth !== undefined) { document.getElementById('maxDepth').value = config.maxDepth; } if (config.concurrentRequests !== undefined) { document.getElementById('concurrentRequests').value = config.concurrentRequests; } if (config.skipPaywalled !== undefined) { document.getElementById('skipPaywalled').checked = config.skipPaywalled; } if (config.followPagination !== undefined) { document.getElementById('followPagination').checked = config.followPagination; } if (config.obeyRobotsTxt !== undefined) { document.getElementById('obeyRobotsTxt').checked = config.obeyRobotsTxt; } if (config.articleListSelector) { document.getElementById('articleListSelector').value = config.articleListSelector; } if (config.titleSelector) { document.getElementById('titleSelector').value = config.titleSelector; } if (config.authorSelector) { document.getElementById('authorSelector').value = config.authorSelector; } if (config.contentSelector) { document.getElementById('contentSelector').value = config.contentSelector; } } // 新增 URL 項目 function addUrlItem(url = '') { const urlList = document.getElementById('urlList'); if (!urlList) return; const urlItem = document.createElement('div'); urlItem.className = 'url-item'; urlItem.innerHTML = ` `; urlItem.querySelector('.btn-remove-url').addEventListener('click', function() { urlItem.remove(); }); urlList.appendChild(urlItem); } // 儲存爬蟲設定 async function saveCrawlerConfig() { try { const config = { urls: getUrlList(), downloadDelay: parseFloat(document.getElementById('downloadDelay').value) || 1, maxDepth: parseInt(document.getElementById('maxDepth').value) || 3, concurrentRequests: parseInt(document.getElementById('concurrentRequests').value) || 16, skipPaywalled: document.getElementById('skipPaywalled').checked, followPagination: document.getElementById('followPagination').checked, obeyRobotsTxt: document.getElementById('obeyRobotsTxt').checked, articleListSelector: document.getElementById('articleListSelector').value, titleSelector: document.getElementById('titleSelector').value, authorSelector: document.getElementById('authorSelector').value, contentSelector: document.getElementById('contentSelector').value }; // 驗證設定 if (config.urls.length === 0) { showConfigStatus('請至少新增一個起始 URL', 'error'); return; } // 儲存到 localStorage localStorage.setItem('crawlerConfig', JSON.stringify(config)); // 同時儲存到伺服器(可選) try { const response = await fetch('/api/save-crawler-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const result = await response.json(); if (result.success) { showConfigStatus('設定已儲存到伺服器', 'success'); } else { showConfigStatus('設定已儲存到本地,但伺服器儲存失敗: ' + result.error, 'error'); } } catch (error) { // 如果伺服器儲存失敗,至少本地已儲存 showConfigStatus('設定已儲存到本地', 'info'); } showConfigStatus('設定已儲存', 'success'); } catch (error) { console.error('儲存設定失敗:', error); showConfigStatus('儲存設定失敗: ' + error.message, 'error'); } } // 取得 URL 列表 function getUrlList() { const urlInputs = document.querySelectorAll('.url-input'); const urls = []; urlInputs.forEach(input => { const url = input.value.trim(); if (url) { urls.push(url); } }); return urls; } // 重置爬蟲設定 function resetCrawlerConfig() { if (confirm('確定要重置為預設設定嗎?')) { loadDefaultConfig(); showConfigStatus('已重置為預設設定', 'info'); } } // 測試爬蟲設定 async function testCrawlerConfig() { const config = { urls: getUrlList(), downloadDelay: parseFloat(document.getElementById('downloadDelay').value) || 1, maxDepth: parseInt(document.getElementById('maxDepth').value) || 3, concurrentRequests: parseInt(document.getElementById('concurrentRequests').value) || 16, skipPaywalled: document.getElementById('skipPaywalled').checked, followPagination: document.getElementById('followPagination').checked, obeyRobotsTxt: document.getElementById('obeyRobotsTxt').checked, articleListSelector: document.getElementById('articleListSelector').value, titleSelector: document.getElementById('titleSelector').value, authorSelector: document.getElementById('authorSelector').value, contentSelector: document.getElementById('contentSelector').value }; if (config.urls.length === 0) { showConfigStatus('請至少新增一個起始 URL', 'error'); return; } showConfigStatus('正在測試設定...', 'info'); try { const response = await fetch('/api/test-crawler-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const result = await response.json(); if (result.success) { if (result.data.articles_found > 0) { showConfigStatus('測試成功!找到 ' + result.data.articles_found + ' 篇文章', 'success'); } else { const warning = result.data.warning || '未找到文章'; showConfigStatus('測試完成,但 ' + warning + '。請檢查 CSS 選擇器設定或查看詳細輸出。', 'error'); } } else { showConfigStatus('測試失敗: ' + result.error, 'error'); } } catch (error) { console.error('測試設定失敗:', error); showConfigStatus('測試設定失敗: ' + error.message, 'error'); } } // 顯示設定狀態訊息 function showConfigStatus(message, type = 'info') { const statusDiv = document.getElementById('configStatus'); if (statusDiv) { statusDiv.textContent = message; statusDiv.className = `config-status ${type}`; statusDiv.style.display = 'block'; // 3 秒後自動隱藏(成功訊息) if (type === 'success') { setTimeout(() => { statusDiv.style.display = 'none'; }, 3000); } } }