- Scrapy 爬蟲框架,爬取 HBR 繁體中文文章 - Flask Web 應用程式,提供文章查詢介面 - SQL Server 資料庫整合 - 自動化排程與郵件通知功能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
728 lines
26 KiB
JavaScript
728 lines
26 KiB
JavaScript
// 全域變數
|
|
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 = '<div style="text-align: center; padding: 40px; color: #666;">沒有找到符合條件的文章</div>';
|
|
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 = `
|
|
<div class="article-title">${title}</div>
|
|
<div class="article-meta">
|
|
${author ? `<span>👤 ${author}</span>` : ''}
|
|
${article.publish_date ? `<span>📅 ${formatDate(article.publish_date)}</span>` : ''}
|
|
${category ? `<span>📁 ${category}</span>` : ''}
|
|
${article.is_paywalled ? '<span class="paywall-badge">💰 付費文章</span>' : ''}
|
|
</div>
|
|
${summary ? `<div class="article-summary">${summary}</div>` : ''}
|
|
${tags.length > 0 ? `<div class="article-tags">${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
|
|
`;
|
|
|
|
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 = `
|
|
<h2 class="article-detail-title">${escapeHtml(article.title || '無標題')}</h2>
|
|
<div class="article-meta">
|
|
${article.author ? `<span>👤 作者: ${escapeHtml(article.author)}</span>` : ''}
|
|
${article.publish_date ? `<span>📅 發布日期: ${formatDate(article.publish_date)}</span>` : ''}
|
|
${article.category ? `<span>📁 分類: ${escapeHtml(article.category)}</span>` : ''}
|
|
${article.is_paywalled ? '<span class="paywall-badge">💰 付費文章</span>' : ''}
|
|
</div>
|
|
${article.summary ? `<div style="margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 5px;"><strong>摘要:</strong>${escapeHtml(article.summary)}</div>` : ''}
|
|
${tags.length > 0 ? `<div style="margin: 15px 0;">標籤: ${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
|
|
${article.content ? `<div class="article-detail-content">${escapeHtml(article.content)}</div>` : '<div style="color: #999; font-style: italic;">此文章無內容(可能是付費文章)</div>'}
|
|
<div style="margin-top: 20px;">
|
|
<a href="${article.url}" target="_blank" class="btn btn-primary">🔗 查看原文</a>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<input type="text" class="url-input" value="${escapeHtml(url)}" placeholder="https://www.hbrtaiwan.com/...">
|
|
<button type="button" class="btn-remove-url">🗑️ 刪除</button>
|
|
`;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|