'use strict'; (function () { const state = { shops: [], folderNameByShopId: new Map(), statusByShopId: new Map(), }; let pollTimer = null; let crawlRoot = ''; let initialized = false; function showToast(msg, type) { const wrap = document.getElementById('toast-wrap'); const el = document.createElement('div'); el.className = 'toast toast-' + (type || 'success'); el.textContent = msg; wrap.appendChild(el); setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; }, 2400); setTimeout(() => { if (el.parentNode) wrap.removeChild(el); }, 2800); } function jsonFetch(url, opts) { return fetch(url, opts).then((r) => r.json().then((data) => { if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); return data; }) ); } function renderShopTable() { const body = document.getElementById('shopTableBody'); body.innerHTML = ''; document.getElementById('shopTableEmpty').style.display = state.shops.length ? 'none' : 'block'; const countEl = document.getElementById('shopCount'); if (countEl) countEl.textContent = state.shops.length + ' 家'; for (const shop of state.shops) { const tr = document.createElement('tr'); const tdCb = document.createElement('td'); const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true; cb.dataset.shopId = String(shop.shopId); tdCb.appendChild(cb); tr.appendChild(tdCb); const tdName = document.createElement('td'); tdName.innerHTML = `
${shop.shopName}
`; tr.appendChild(tdName); const tdId = document.createElement('td'); tdId.textContent = shop.shopId; tdId.className = 'num'; tr.appendChild(tdId); const tdStatus = document.createElement('td'); const st = state.statusByShopId.get(shop.shopId) || (shop.completed ? { state: 'skipped_completed' } : null); tdStatus.innerHTML = st ? statusBadgeHtml(st) : '待抓取'; tr.appendChild(tdStatus); const saved = shop.completed; const folderName = state.folderNameByShopId.get(shop.shopId) || (saved ? saved.folderName : null); const tdFolder = document.createElement('td'); tdFolder.className = 'col-folder'; tdFolder.innerHTML = folderName ? `${folderName}` : ''; tr.appendChild(tdFolder); tr.dataset.shopId = String(shop.shopId); body.appendChild(tr); } } function statusBadgeHtml(st) { const label = { pending: '排队中', running: '抓取中', done: `完成 ${st.downloaded}/${st.total || 0}`, skipped_completed: '已完成', failed: '失败', stopped: '已停止', }[st.state] || st.state; const cls = { done: 'badge-ok', failed: 'badge-fail', running: 'badge-warn' }[st.state] || 'badge-info'; return `${label}`; } const ICON_PLAY = ''; function renderCrawlState(cs) { const badge = document.getElementById('crawlStatusBadge'); const labelMap = { idle: '空闲', running: '抓取中', done: '已完成', stopped: '已停止', error: '出错' }; const clsMap = { running: 'badge-warn', done: 'badge-ok', error: 'badge-fail', stopped: 'badge-warn' }; badge.textContent = labelMap[cs.status] || cs.status; badge.className = 'badge ' + (clsMap[cs.status] || ''); const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); const startIcon = document.getElementById('startBtnIcon'); const startText = document.getElementById('startBtnText'); startBtn.disabled = cs.status === 'running'; stopBtn.style.display = cs.status === 'running' ? '' : 'none'; if (cs.status === 'running') { startIcon.innerHTML = ''; startText.textContent = '抓取中… 点击停止'; startBtn.classList.remove('btn-primary'); startBtn.classList.add('btn-ghost'); startBtn.disabled = false; } else { startIcon.innerHTML = ICON_PLAY; startText.textContent = '开始抓取'; startBtn.classList.remove('btn-ghost'); startBtn.classList.add('btn-primary'); } state.statusByShopId.clear(); state.folderNameByShopId.clear(); for (const s of cs.shops) { state.statusByShopId.set(s.shopId, s); if (s.folderName) state.folderNameByShopId.set(s.shopId, s.folderName); } renderShopTable(); const list = document.getElementById('progressList'); list.innerHTML = ''; if (!cs.shops.length) { list.innerHTML = '

暂无任务

'; } for (const s of cs.shops) { const row = document.createElement('div'); row.className = 'progress-row'; const pct = s.total > 0 ? Math.round((s.downloaded + s.skipped) / s.total * 100) : 0; row.innerHTML = `
采集${s.shopName}· 菜品图
${s.downloaded + s.skipped} / ${s.total || 0} ${statusBadgeHtml(s)}
`; list.appendChild(row); } if (cs.status !== 'running' && pollTimer) { clearInterval(pollTimer); pollTimer = null; } } function startPolling() { if (pollTimer) clearInterval(pollTimer); const tick = () => jsonFetch('/api/crawl/status').then(renderCrawlState).catch(() => {}); tick(); pollTimer = setInterval(tick, 1500); } function refreshShops() { const root = crawlRoot; const query = root ? `?root=${btoa(unescape(encodeURIComponent(root))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}` : ''; return jsonFetch(`/api/shops${query}`).then((data) => { state.shops = data.shops || []; renderShopTable(); }); } window.shanCrawlInit = function () { if (initialized) return; initialized = true; const modal = document.getElementById('modalAddShop'); document.getElementById('btnToggleAdd').addEventListener('click', () => { modal.hidden = false; document.getElementById('manualInput').focus(); }); document.getElementById('btnCloseModal').addEventListener('click', () => { modal.hidden = true; }); document.getElementById('btnCancelAdd').addEventListener('click', () => { modal.hidden = true; }); modal.addEventListener('click', (e) => { if (e.target === modal) modal.hidden = true; }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !modal.hidden) modal.hidden = true; }); document.getElementById('advToggle').addEventListener('click', function () { const open = this.getAttribute('aria-expanded') === 'true'; this.setAttribute('aria-expanded', String(!open)); document.getElementById('advBody').classList.toggle('is-open', !open); }); document.getElementById('btnClearLog').addEventListener('click', () => { document.getElementById('logPanel').innerHTML = ''; }); jsonFetch('/api/settings').then(s => { crawlRoot = s.rootDir; return refreshShops(); }); jsonFetch('/api/crawl/status').then((cs) => { renderCrawlState(cs); if (cs.status === 'running') startPolling(); }); document.getElementById('useManualBtn').addEventListener('click', () => { const text = document.getElementById('manualInput').value; if (!text.trim()) return showToast('请先填写手动清单', 'error'); jsonFetch('/api/shops/manual', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }).then((data) => { state.shops = data.shops; renderShopTable(); showToast(`已添加 ${data.addedCount} 家店铺`); }).catch((e) => showToast('添加失败: ' + e.message, 'error')); }); document.getElementById('selectAllCb').addEventListener('change', (e) => { document.querySelectorAll('#shopTableBody input[type=checkbox]').forEach((cb) => { cb.checked = e.target.checked; }); }); document.getElementById('startBtn').addEventListener('click', () => { if (document.getElementById('stopBtn').style.display !== 'none') { jsonFetch('/api/crawl/stop', { method: 'POST' }).then(() => showToast('已发送停止请求')); return; } const shopIds = Array.from(document.querySelectorAll('#shopTableBody input[type=checkbox]')) .filter((cb) => cb.checked).map((cb) => Number(cb.dataset.shopId)); if (!shopIds.length) return showToast('请先在店铺列表里勾选要抓取的店铺', 'error'); const root = crawlRoot; jsonFetch('/api/crawl/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ root, shopIds, forceFullCrawl: document.getElementById('forceFullCrawl').checked }), }).then(() => { showToast('已开始抓取'); startPolling(); }) .catch((e) => showToast('启动失败: ' + e.message, 'error')); }); document.getElementById('stopBtn').addEventListener('click', () => { jsonFetch('/api/crawl/stop', { method: 'POST' }).then(() => showToast('已发送停止请求')); }); }; })();