/* ═══════════════════════════════════════════════════════════ * page-dashboard.js — Dashboard 互動腳本 * 從原 dashboard.html L1118-L1547 抽出 * 變更: * - Chart.js 漸層色從 #667eea/#764ba2 改讀 CSS var(--momo-page-accent) * - tooltip border / pointBackground 使用群組色 * ═══════════════════════════════════════════════════════════ */ // 讀取 CSS var 的小工具 function getMomoVar(name, fallback) { const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } // Bootstrap Tooltips var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); tooltipTriggerList.map(function (el) { return new bootstrap.Tooltip(el); }); function getCSRFToken() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } function triggerTask() { if (confirm('確定要手動執行全站爬蟲嗎?(可能需要一段時間)')) { fetch('/api/run_task', { method: 'POST', headers: { 'X-CSRFToken': getCSRFToken() } }) .then(r => r.json()).then(d => alert(d.message)).catch(e => alert('錯誤: ' + e)); } } function triggerNotification() { if (confirm('確定要發送今日商品異動通知嗎?')) { fetch('/api/trigger_momo_notification', { method: 'POST', headers: { 'X-CSRFToken': getCSRFToken() } }) .then(r => r.json()).then(d => alert(d.message)).catch(e => alert('錯誤: ' + e)); } } let priceChartInstance = null; function handleRowClick(event, productId, row) { if (event.target.closest('a') || event.target.closest('button')) return; showHistory(productId, row.getAttribute('data-name')); } function showHistory(productId, productName) { if (typeof Chart === 'undefined') { alert('圖表元件尚未載入完成'); return; } const modalEl = document.getElementById('historyModal'); let modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl); document.getElementById('historyModalLabel').innerText = productName; modal.show(); if (priceChartInstance) { priceChartInstance.destroy(); priceChartInstance = null; } const accent = getMomoVar('--momo-page-accent', '#D97757'); const accentDark = getMomoVar('--momo-page-accent-dark', '#A85A3F'); const accentSoft = getMomoVar('--momo-page-accent-soft', 'rgba(217,119,87,0.12)'); fetch(`/api/history/${productId}`).then(r => r.json()).then(data => { const ctx = document.getElementById('priceChart').getContext('2d'); priceChartInstance = new Chart(ctx, { type: 'line', data: { labels: data.map(d => d.t), datasets: [{ label: '價格', data: data.map(d => d.p), borderColor: accent, backgroundColor: accentSoft, borderWidth: 2, fill: true, tension: 0.35, pointRadius: 3, pointHoverRadius: 6, pointBackgroundColor: accent, pointBorderColor: '#fff8ef', pointBorderWidth: 2, pointHoverBackgroundColor: accentDark, pointHoverBorderColor: '#fff8ef' }] }, options: { responsive: true, maintainAspectRatio: true, interaction: { mode: 'index', intersect: false }, plugins: { tooltip: { backgroundColor: '#29261b', titleColor: '#fff8ef', bodyColor: '#fff8ef', borderColor: accent, borderWidth: 1, padding: 10, displayColors: false, callbacks: { label: c => ' $' + c.parsed.y.toLocaleString() } }, legend: { display: false } }, scales: { y: { beginAtZero: false, grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false }, ticks: { color: accentDark, font: { weight: '600' }, callback: v => '$' + v.toLocaleString() } }, x: { grid: { display: false }, ticks: { color: '#6c757d', font: { size: 11 } } } }, animation: { duration: 600, easing: 'easeInOutQuart' } } }); }).catch(err => console.error("圖表載入失敗:", err)); } function copyToClipboard(event, text, element) { event.stopPropagation(); const showFeedback = () => { const orig = element.innerHTML; element.innerHTML = '✅ 已複製!'; element.style.transform = 'scale(1.1)'; element.style.transition = 'all 0.3s ease'; setTimeout(() => { element.innerHTML = orig; element.style.transform = 'scale(1)'; }, 1500); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(showFeedback); } else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); showFeedback(); } catch (err) { console.error('複製失敗', err); } document.body.removeChild(ta); } } let currentFilterType = ''; let currentFilterCategory = ''; function showPriceChangeModal(type, title, productId = '') { currentFilterType = type; currentFilterCategory = (type === 'category') ? title : ''; const modalEl = document.getElementById('priceChangeModal'); let modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl); document.getElementById('priceChangeModalLabel').innerText = title; modal.show(); const tbody = document.getElementById('modalProductList'); tbody.innerHTML = '