/* ═══════════════════════════════════════════════════════════
* 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 = `
${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 = '