// 全域變數
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);
}
}
}