/* ═══════════════════════════════════════════════════════════ * page-settings.js — 爬蟲管理中心 * 從原 settings.html L1230-L1648 抽出 * 邏輯與原版一致 * ═══════════════════════════════════════════════════════════ */ let categoryModal; document.addEventListener('DOMContentLoaded', function () { categoryModal = new bootstrap.Modal(document.getElementById('categoryModal')); loadCrawlers(); // 顯示搜尋框(如果有分類資料) const grid = document.getElementById('categories-grid'); const searchContainer = document.getElementById('search-container'); if (grid && grid.children.length > 0) { searchContainer.style.display = 'block'; } }); // ───────── Toast / Loading ───────── function showToast(message, type = 'success') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast-notification ${type}`; const icon = type === 'success' ? 'check-circle' : 'exclamation-circle'; toast.innerHTML = `
${message}
`; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideIn 0.25s ease-out reverse'; setTimeout(() => toast.remove(), 250); }, 4000); } function showLoading() { document.getElementById('loading-overlay').classList.add('active'); } function hideLoading() { document.getElementById('loading-overlay').classList.remove('active'); } function getCSRFToken() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } // ───────── Crawler ───────── async function loadCrawlers() { try { const r = await fetch('/api/crawlers'); const result = await r.json(); if (result.status === 'success') { renderCrawlers(result.data); updateStats(result.data); } else { showToast('載入失敗: ' + result.message, 'error'); } } catch (e) { showToast('載入爬蟲配置時發生錯誤', 'error'); console.error(e); } } function renderCrawlers(crawlers) { const container = document.getElementById('crawlers-container'); container.innerHTML = ''; for (const [key, info] of Object.entries(crawlers)) { container.appendChild(createCrawlerCard(key, info)); } } function createCrawlerCard(key, info) { const card = document.createElement('div'); card.className = `crawler-card ${info.enabled ? 'active' : 'paused'}`; const statusClass = info.enabled ? 'active' : 'paused'; const statusText = info.enabled ? '運行中' : '已暫停'; card.innerHTML = `
${info.name}
${statusText}
${info.description || 'N/A'}
每 ${info.schedule_hours || 'N/A'} 小時執行
${info.lpn_code ? `
活動代碼:${info.lpn_code}
` : ''} ${info.last_active_date ? `
最後活動:${info.last_active_date}
` : ''}
${!info.enabled && info.pause_reason ? `
暫停原因 ${info.pause_reason} ${info.notes ? `
${info.notes}` : ''}
` : ''} ${info.enabled ? `
` : ''}
`; return card; } function updateStats(crawlers) { const total = Object.keys(crawlers).length; const enabled = Object.values(crawlers).filter(c => c.enabled).length; document.getElementById('enabled-count').textContent = enabled; document.getElementById('paused-count').textContent = total - enabled; document.getElementById('total-count').textContent = total; } async function toggleCrawler(key, enabled) { let reason = ''; if (!enabled) reason = prompt('請輸入停用原因(選填):') || '手動停用'; showLoading(); try { const r = await fetch(`/api/crawlers/${key}/toggle`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, reason }) }); const result = await r.json(); if (result.status === 'success') { showToast(result.message, 'success'); } else { showToast('操作失敗: ' + result.message, 'error'); } await loadCrawlers(); } catch (e) { showToast('操作時發生錯誤', 'error'); console.error(e); await loadCrawlers(); } finally { hideLoading(); } } async function changeSchedule(key, currentHours) { const newHours = prompt(`請輸入新的執行頻率(小時,範圍:1-24)\n\n目前設定:每 ${currentHours} 小時`, currentHours); if (newHours === null || newHours === currentHours.toString()) return; const hours = parseInt(newHours); if (isNaN(hours) || hours < 1 || hours > 24) { showToast('請輸入有效的小時數(1-24)', 'error'); return; } showLoading(); try { const r = await fetch(`/api/crawlers/${key}/schedule`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schedule_hours: hours }) }); const result = await r.json(); if (result.status === 'success') { showToast(result.message + '(需重啟排程器生效)', 'success'); await loadCrawlers(); } else { showToast('更新失敗: ' + result.message, 'error'); } } catch (e) { showToast('更新時發生錯誤', 'error'); console.error(e); } finally { hideLoading(); } } async function refreshData() { const btn = event.target.closest('.refresh-btn'); btn.classList.add('spinning'); btn.disabled = true; await loadCrawlers(); setTimeout(() => { btn.classList.remove('spinning'); btn.disabled = false; showToast('已刷新爬蟲狀態', 'success'); }, 600); } // ───────── Category ───────── function prepareAddModal() { document.getElementById('modalTitle').innerText = '新增分類'; document.getElementById('categoryId').value = ''; document.getElementById('categoryName').value = ''; document.getElementById('categoryUrl').value = ''; categoryModal.show(); } function prepareEditModal(id, name, url) { document.getElementById('modalTitle').innerText = '編輯分類'; document.getElementById('categoryId').value = id; document.getElementById('categoryName').value = name; document.getElementById('categoryUrl').value = url; categoryModal.show(); } function saveCategory() { const id = document.getElementById('categoryId').value; const name = document.getElementById('categoryName').value; const url = document.getElementById('categoryUrl').value; const method = id ? 'PUT' : 'POST'; const endpoint = id ? `/api/categories/${id}` : '/api/categories'; const formData = new FormData(); formData.append('name', name); formData.append('url', url); showLoading(); fetch(endpoint, { method, headers: { 'X-CSRFToken': getCSRFToken() }, body: formData }) .then(r => r.json()) .then(d => { if (d.status === 'success') { categoryModal.hide(); showToast(id ? '分類已更新' : '分類已新增', 'success'); setTimeout(() => location.reload(), 800); } else { showToast('操作失敗: ' + d.message, 'error'); } }) .catch(e => { console.error(e); showToast('發生錯誤,請稍後再試', 'error'); }) .finally(hideLoading); } function deleteCategory(id) { if (!confirm('確定要刪除此分類嗎?此操作無法復原。')) return; showLoading(); fetch(`/api/categories/${id}`, { method: 'DELETE', headers: { 'X-CSRFToken': getCSRFToken() } }) .then(r => r.json()) .then(d => { if (d.status === 'success') { showToast('分類已刪除', 'success'); setTimeout(() => location.reload(), 800); } else { showToast('刪除失敗: ' + d.message, 'error'); } }) .catch(e => { console.error(e); showToast('發生錯誤,請稍後再試', 'error'); }) .finally(hideLoading); } function testUrl(url, btn) { const orig = btn.innerHTML; btn.innerHTML = ''; btn.disabled = true; fetch('/api/test_url', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCSRFToken() }, body: JSON.stringify({ url }) }) .then(r => r.json()) .then(d => showToast(d.message, d.status === 'success' ? 'success' : 'error')) .catch(e => { console.error(e); showToast('測試請求失敗', 'error'); }) .finally(() => { btn.innerHTML = orig; btn.disabled = false; }); } // ───────── Filter ───────── function filterCategories() { const input = document.getElementById('category-search'); const box = document.getElementById('search-box'); const term = input.value.toLowerCase().trim(); const cards = document.querySelectorAll('.category-card'); const noResults = document.getElementById('no-results'); let visible = 0; box.classList.toggle('has-text', !!term); cards.forEach(card => { const m = card.dataset.categoryName.includes(term) || card.dataset.categoryUrl.includes(term); card.style.display = m ? '' : 'none'; if (m) visible++; }); noResults.style.display = (visible === 0 && term) ? 'block' : 'none'; } function clearSearch() { const input = document.getElementById('category-search'); document.getElementById('search-box').classList.remove('has-text'); input.value = ''; filterCategories(); input.focus(); }