Files
ewoooc/web/static/js/page-settings.js
ogt d2d798933a
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s
fix: rename crawler UI to product monitoring
2026-06-26 18:08:14 +08:00

279 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════════
* 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 = `
<div class="toast-icon"><i class="fas fa-${icon}"></i></div>
<div class="toast-content"><div class="toast-message">${message}</div></div>
`;
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 = `
<div class="crawler-header">
<div class="crawler-title-group">
<div class="crawler-title"><i class="fas fa-robot"></i>${info.name}</div>
<span class="status-badge ${statusClass}"><i class="fas fa-circle"></i>${statusText}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" ${info.enabled ? 'checked' : ''} onchange="toggleCrawler('${key}', this.checked)">
<span class="slider"></span>
</label>
</div>
<div class="crawler-body">
<div class="crawler-info">
<div class="info-item"><i class="fas fa-info-circle"></i><span>${info.description || 'N/A'}</span></div>
<div class="info-item"><i class="fas fa-clock"></i><span>每 ${info.schedule_hours || 'N/A'} 小時執行</span></div>
${info.lpn_code ? `<div class="info-item"><i class="fas fa-barcode"></i><span>活動代碼:${info.lpn_code}</span></div>` : ''}
${info.last_active_date ? `<div class="info-item"><i class="fas fa-calendar-check"></i><span>最後活動:${info.last_active_date}</span></div>` : ''}
</div>
${!info.enabled && info.pause_reason ? `
<div class="pause-reason">
<strong><i class="fas fa-pause-circle me-2"></i>暫停原因</strong>
${info.pause_reason}
${info.notes ? `<br><small><i class="fas fa-sticky-note me-1"></i>${info.notes}</small>` : ''}
</div>` : ''}
${info.enabled ? `
<div class="crawler-controls">
<button class="btn-custom btn-schedule" onclick="changeSchedule('${key}', ${info.schedule_hours})">
<i class="fas fa-edit"></i>修改頻率
</button>
</div>` : ''}
</div>
`;
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 = '<i class="fas fa-spinner fa-spin"></i>';
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();
}