diff --git a/config.py b/config.py index 63f59b7..b37543e 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.160" +SYSTEM_VERSION = "V10.161" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/templates/edm_dashboard_v2.html b/templates/edm_dashboard_v2.html index eba2aa4..918fcc9 100644 --- a/templates/edm_dashboard_v2.html +++ b/templates/edm_dashboard_v2.html @@ -3,751 +3,8 @@ {% block title %}EwoooC 活動看板{% endblock %} {% block extra_css %} - + - {% endblock %} {% block ewooo_content %} @@ -934,7 +191,7 @@ {% for item in items %} {% set campaign_filter = 'new' if item.status_change == 'NEW' else ('up' if item.status_change == 'PRICE_UP' else ('down' if item.status_change == 'PRICE_DOWN' else ('delisted' if item.status_change in ['DELISTED', 'SLOT_END'] else 'active'))) %} - + {{ item.main_category or '未分類' }} @@ -993,8 +250,7 @@ data-campaign-history-trigger data-i-code="{{ item.i_code }}" data-product-name="{{ item.name|e }}" - onclick="event.stopPropagation(); showCampaignHistory(this.dataset.iCode, this.dataset.productName);" - aria-label="查看 {{ item.name|e }} 的歷史價格圖表" + aria-label="查看價格歷史圖表" > ${{ item.price | number_format }} @@ -1006,8 +262,7 @@ data-campaign-history-trigger data-i-code="{{ item.i_code }}" data-product-name="{{ item.name|e }}" - onclick="event.stopPropagation(); showCampaignHistory(this.dataset.iCode, this.dataset.productName);" - aria-label="查看 {{ item.name|e }} 的歷史價格圖表" + aria-label="查看價格歷史圖表" > ${{ item.price | number_format }} @@ -1110,424 +365,5 @@ {% endblock %} {% block extra_js %} - + {% endblock %} diff --git a/web/static/css/page-edm-v2.css b/web/static/css/page-edm-v2.css new file mode 100644 index 0000000..c927a84 --- /dev/null +++ b/web/static/css/page-edm-v2.css @@ -0,0 +1,742 @@ +/* EDM V2 page-specific styles extracted from edm_dashboard_v2.html. */ +.campaign-stack { + display: grid; + gap: 24px; + } + + .campaign-switcher { + display: inline-flex; + max-width: 100%; + padding: 4px; + gap: 2px; + overflow-x: auto; + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 6px; + } + + .campaign-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + color: var(--momo-text-secondary); + border-radius: 4px; + font-size: 13px; + font-weight: 800; + text-decoration: none; + white-space: nowrap; + transition: var(--momo-transition-base); + } + + .campaign-tab:hover { + color: var(--momo-text-primary); + background: var(--momo-bg-paper); + } + + .campaign-tab.is-active { + color: var(--momo-text-inverse); + background: var(--momo-ink); + } + + .campaign-tab-count { + padding: 1px 6px; + color: var(--momo-text-tertiary); + background: var(--momo-bg-subtle); + border-radius: var(--momo-radius-pill); + font-size: 10px; + font-weight: 800; + } + + .campaign-tab.is-active .campaign-tab-count { + color: var(--momo-text-inverse); + background: rgba(250, 247, 240, 0.22); + } + + .campaign-hero-grid { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); + gap: 12px; + } + + .campaign-hero { + position: relative; + min-height: 220px; + overflow: hidden; + padding: 28px 32px; + color: var(--momo-text-primary); + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-light); + border-radius: 8px; + --campaign-accent: var(--momo-warm-caramel); + } + + .campaign-hero.is-edm { + --campaign-accent: var(--momo-warm-caramel); + } + + .campaign-hero.is-festival { + --campaign-accent: var(--momo-warm-honey); + } + + .campaign-hero.is-mothers-day { + --campaign-accent: var(--momo-warm-rust); + } + + .campaign-hero.is-valentine-520 { + --campaign-accent: var(--momo-warm-mahogany); + } + + .campaign-hero.is-labor-day { + --campaign-accent: var(--momo-warm-earth); + } + + .campaign-hero::before { + position: absolute; + inset: 0; + content: ""; + background-image: radial-gradient(circle, rgba(42, 37, 32, 0.12) 1px, transparent 1px); + background-size: 8px 8px; + opacity: 0.6; + } + + .campaign-hero::after { + position: absolute; + top: 0; + left: 0; + width: 64px; + height: 4px; + content: ""; + background: var(--campaign-accent); + } + + .campaign-hero-content { + position: relative; + display: flex; + min-height: 172px; + flex-direction: column; + gap: 16px; + } + + .campaign-hero-content::before { + position: absolute; + top: -28px; + left: -32px; + width: 4px; + height: 64px; + content: ""; + background: var(--campaign-accent); + } + + .campaign-eyebrow { + display: flex; + align-items: center; + gap: 8px; + } + + .campaign-eyebrow-label { + padding: 3px 10px; + color: var(--campaign-accent); + border: 1px solid var(--campaign-accent); + border-radius: 2px; + background: var(--momo-bg-surface); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + .campaign-title { + margin: 0; + color: var(--momo-text-primary); + font-family: var(--momo-font-display); + font-size: 40px; + font-weight: 800; + letter-spacing: 0; + line-height: 1.05; + } + + .campaign-meta-grid { + display: flex; + gap: 24px; + flex-wrap: wrap; + font-size: 12px; + } + + .campaign-meta-label { + margin-bottom: 2px; + color: var(--momo-text-tertiary); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.10em; + text-transform: uppercase; + } + + .campaign-meta-value { + color: var(--momo-text-primary); + font-weight: 800; + } + + .campaign-actions { + display: flex; + gap: 8px; + margin-top: auto; + flex-wrap: wrap; + } + + .campaign-action { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 32px; + padding: 8px 14px; + color: var(--momo-text-primary); + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 4px; + font-size: 13px; + font-weight: 800; + } + + .campaign-action.is-light { + color: var(--momo-text-inverse); + background: var(--momo-ink); + border-color: var(--momo-ink); + } + + .campaign-kpi-panel { + display: flex; + flex-direction: column; + min-height: 220px; + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 8px; + } + + .campaign-panel-title { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 20px; + border-bottom: 1px solid var(--momo-border-light); + font-size: 13px; + font-weight: 800; + } + + .campaign-kpi-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + padding: 0; + flex: 1; + } + + .campaign-kpi { + padding: 20px; + background: transparent; + border-right: 1px solid var(--momo-border-light); + border-bottom: 1px solid var(--momo-border-light); + border-radius: 0; + } + + .campaign-kpi:nth-child(2n) { + border-right: 0; + } + + .campaign-kpi:nth-last-child(-n+2) { + border-bottom: 0; + } + + .campaign-kpi-label { + margin-bottom: 4px; + color: var(--momo-text-secondary); + font-family: var(--momo-font-family-mono); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.10em; + text-transform: uppercase; + } + + .campaign-kpi-value { + color: var(--momo-text-primary); + font-size: 26px; + font-weight: 800; + letter-spacing: 0; + line-height: 1; + } + + .campaign-panel-footer { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + color: var(--momo-text-secondary); + background: var(--momo-bg-paper); + border-top: 1px solid var(--momo-border-light); + border-radius: 0 0 8px 8px; + font-size: 11px; + } + + .campaign-section-label { + display: flex; + align-items: baseline; + gap: 10px; + margin-bottom: 12px; + } + + .campaign-section-label .num { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--momo-text-tertiary); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + } + + .campaign-section-label .num::after { + display: inline-block; + width: 56px; + height: 6px; + content: ""; + background-image: radial-gradient(circle, var(--momo-text-tertiary) 1px, transparent 1px); + background-size: 6px 6px; + opacity: 0.5; + } + + .campaign-section-label .title { + color: var(--momo-text-primary); + font-size: 13px; + font-weight: 800; + } + + .campaign-slot-tabs { + display: flex; + gap: 8px; + padding-bottom: 4px; + overflow-x: auto; + } + + .campaign-slot-tab { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + color: var(--momo-text-secondary); + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 6px; + font-size: 12px; + font-weight: 800; + text-decoration: none; + white-space: nowrap; + } + + .campaign-slot-tab.active { + color: var(--momo-text-inverse); + background: var(--momo-ink); + border-color: var(--momo-ink); + } + + .campaign-slot-count { + color: inherit; + font-size: 10px; + } + + .campaign-table-card { + overflow: hidden; + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 8px; + } + + .campaign-table-head { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + border-bottom: 1px solid var(--momo-border-light); + flex-wrap: wrap; + } + + .campaign-stat-badges { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .campaign-badge { + display: inline-flex; + align-items: center; + padding: 3px 8px; + color: var(--momo-text-secondary); + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-light); + border-radius: var(--momo-radius-pill); + font-size: 11px; + font-weight: 800; + } + + .campaign-filterbar { + display: inline-flex; + padding: 2px; + gap: 0; + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-light); + border-radius: 4px; + flex-wrap: wrap; + } + + .campaign-filter-chip { + padding: 5px 10px; + color: var(--momo-text-secondary); + background: transparent; + border: 0; + border-radius: 3px; + font-size: 12px; + font-weight: 800; + } + + .campaign-filter-chip:hover { + color: var(--momo-text-primary); + background: var(--momo-bg-subtle); + } + + .campaign-filter-chip.is-active { + color: var(--momo-text-inverse); + background: var(--momo-ink); + } + + .campaign-pagination { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; + color: var(--momo-text-secondary); + font-family: var(--momo-font-family-mono); + font-size: 11px; + font-weight: 800; + } + + .campaign-page-link { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 30px; + padding: 5px 9px; + color: var(--momo-text-primary); + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 6px; + text-decoration: none; + } + + .campaign-page-link[aria-disabled="true"] { + pointer-events: none; + color: var(--momo-text-disabled); + background: var(--momo-bg-paper); + } + + .campaign-table-wrap { + overflow-x: auto; + } + + .campaign-table { + width: 100%; + min-width: 1080px; + border-collapse: collapse; + font-size: var(--momo-font-size-sm); + } + + .campaign-table th { + padding: 11px 14px; + color: var(--momo-text-inverse); + background: var(--momo-ink); + border-bottom: 1px solid var(--momo-border-light); + font-family: var(--momo-font-family-mono); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.10em; + text-transform: uppercase; + white-space: nowrap; + } + + .campaign-table th a { + color: inherit; + text-decoration: none; + } + + .campaign-table td { + padding: 14px; + border-bottom: 1px solid var(--momo-border-light); + vertical-align: middle; + } + + .campaign-table tbody tr:hover { + background: var(--momo-bg-paper); + } + + .campaign-product-cell { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + } + + .campaign-product-thumb { + width: 52px; + height: 52px; + flex: 0 0 auto; + object-fit: cover; + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-light); + border-radius: 6px; + } + + .campaign-product-name { + display: -webkit-box; + overflow: hidden; + color: var(--momo-text-primary); + font-weight: 800; + line-height: 1.35; + text-decoration: none; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .campaign-product-name:hover { + color: var(--momo-accent); + } + + .campaign-product-id { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 4px; + color: var(--momo-text-tertiary); + background: transparent; + border: 0; + font-size: 11px; + font-family: var(--momo-font-family-mono); + font-weight: 800; + line-height: 1.4; + } + + .campaign-product-id:hover { + color: var(--momo-accent-strong); + } + + .campaign-product-meta { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; + flex-wrap: wrap; + } + + .campaign-category { + display: inline-flex; + max-width: 120px; + padding: 3px 8px; + overflow: hidden; + color: var(--momo-text-primary); + background: var(--momo-bg-subtle); + border: 1px solid var(--momo-border-light); + border-radius: var(--momo-radius-pill); + font-size: 11px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; + } + + .campaign-price { + color: var(--momo-text-primary); + font-size: 16px; + font-weight: 800; + } + + .campaign-history-button { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + width: 100%; + padding: 0; + color: var(--momo-text-primary); + background: transparent; + border: 0; + font: inherit; + text-align: right; + } + + .campaign-history-button:hover { + color: var(--momo-accent-strong); + } + + .campaign-history-button i { + color: var(--momo-accent-strong); + font-size: 12px; + } + + .campaign-old-price { + color: var(--momo-text-tertiary); + text-decoration: line-through; + } + + .campaign-change-up { + color: var(--momo-danger); + font-weight: 800; + } + + .campaign-change-down { + color: var(--momo-success); + font-weight: 800; + } + + .campaign-change-pct { + margin-left: 4px; + color: currentColor; + opacity: 0.72; + font-size: 11px; + } + + .campaign-sales-stack, + .campaign-track-stack { + display: grid; + gap: 6px; + } + + .campaign-sales-main { + color: var(--momo-text-primary); + font-size: 14px; + font-weight: 900; + } + + .campaign-sales-sub, + .campaign-track-line { + color: var(--momo-text-tertiary); + font-size: 11px; + font-weight: 800; + } + + .campaign-tooltip-trigger { + width: max-content; + color: var(--momo-accent-strong); + background: transparent; + border: 0; + font-size: 11px; + font-weight: 900; + text-decoration: underline; + text-underline-offset: 3px; + } + + .campaign-empty { + padding: 48px 16px; + color: var(--momo-text-secondary); + text-align: center; + } + + .campaign-filter-empty.is-hidden, + .campaign-row.is-hidden { + display: none; + } + + .campaign-history-modal .modal-content { + overflow: hidden; + color: var(--momo-text-primary); + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 8px; + box-shadow: var(--momo-shadow-lg); + } + + .campaign-history-modal .modal-header { + align-items: flex-start; + gap: 14px; + padding: 18px 20px; + background: var(--momo-bg-paper); + border-bottom: 1px solid var(--momo-border-light); + } + + .campaign-history-modal .modal-title { + color: var(--momo-text-primary); + font-size: 18px; + font-weight: 800; + line-height: 1.45; + } + + .campaign-history-subtitle { + margin-top: 4px; + color: var(--momo-text-tertiary); + font-size: 11px; + font-weight: 700; + } + + .campaign-history-range { + display: inline-flex; + padding: 2px; + margin-top: 10px; + gap: 0; + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-light); + border-radius: 4px; + } + + .campaign-history-range button { + padding: 5px 10px; + color: var(--momo-text-secondary); + background: transparent; + border: 0; + border-radius: 3px; + font-size: 12px; + font-weight: 800; + } + + .campaign-history-range button.is-active { + color: var(--momo-text-inverse); + background: var(--momo-ink); + } + + .campaign-chart-shell { + position: relative; + min-height: 360px; + } + + .campaign-chart-state { + display: grid; + min-height: 360px; + color: var(--momo-text-secondary); + place-items: center; + text-align: center; + } + + .campaign-chart-state.is-hidden, + .campaign-chart-canvas.is-hidden { + display: none; + } + + .campaign-chart-canvas { + max-height: 380px; + } + + @media (max-width: 980px) { + .campaign-hero-grid { + grid-template-columns: 1fr; + } + } + + @media (max-width: 640px) { + .campaign-title { + font-size: 28px; + } + + .campaign-kpi-grid { + grid-template-columns: 1fr 1fr; + } + + .campaign-pagination { + width: 100%; + margin-left: 0; + justify-content: space-between; + } + } diff --git a/web/static/js/page-edm-v2.js b/web/static/js/page-edm-v2.js new file mode 100644 index 0000000..a57f1bc --- /dev/null +++ b/web/static/js/page-edm-v2.js @@ -0,0 +1,426 @@ +/* EDM V2 page interactions extracted from edm_dashboard_v2.html. */ +let campaignPriceChartInstance = null; + let activeCampaignHistoryRange = 'month'; + let currentCampaignICode = null; + let currentCampaignProductName = ''; + let campaignChartLoader = null; + + function getCSRFToken() { + return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + } + + function copyCampaignProductId(text, element) { + if (!text) return; + const originalHtml = element.innerHTML; + const showFeedback = () => { + element.innerHTML = '已複製 '; + setTimeout(() => { + element.innerHTML = originalHtml; + }, 1200); + }; + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(showFeedback); + return; + } + + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { + document.execCommand('copy'); + showFeedback(); + } finally { + document.body.removeChild(textarea); + } + } + + function formatCampaignPriceTick(value) { + return '$' + Number(value || 0).toLocaleString(); + } + + function setCampaignHistoryChartState(message, showCanvas = false) { + const state = document.getElementById('campaignHistoryChartState'); + const canvas = document.getElementById('campaignPriceChart'); + if (!state || !canvas) return; + state.textContent = message; + state.classList.toggle('is-hidden', showCanvas); + canvas.classList.toggle('is-hidden', !showCanvas); + } + + function destroyCampaignPriceChart() { + if (campaignPriceChartInstance) { + campaignPriceChartInstance.destroy(); + campaignPriceChartInstance = null; + } + } + + function ensureCampaignChart() { + if (typeof Chart !== 'undefined') { + return Promise.resolve(); + } + if (campaignChartLoader) { + return campaignChartLoader; + } + campaignChartLoader = new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; + script.async = true; + script.onload = resolve; + script.onerror = () => reject(new Error('Chart.js 載入失敗')); + document.head.appendChild(script); + }); + return campaignChartLoader; + } + + function updateCampaignHistoryRangeButtons() { + document.querySelectorAll('[data-campaign-history-range]').forEach(button => { + button.classList.toggle('is-active', button.dataset.campaignHistoryRange === activeCampaignHistoryRange); + }); + } + + function showCampaignHistory(iCode, productName, range = activeCampaignHistoryRange) { + const modalEl = document.getElementById('campaignHistoryModal'); + const title = document.getElementById('campaignHistoryModalLabel'); + const subtitle = document.getElementById('campaignHistoryModalSubtitle'); + const canvas = document.getElementById('campaignPriceChart'); + + if (!modalEl || !title || !subtitle || !canvas) return; + + currentCampaignICode = iCode; + currentCampaignProductName = productName || '歷史價格走勢'; + activeCampaignHistoryRange = range; + updateCampaignHistoryRangeButtons(); + + title.textContent = currentCampaignProductName; + subtitle.textContent = `商品 ID ${iCode} · 讀取真實價格紀錄`; + destroyCampaignPriceChart(); + setCampaignHistoryChartState('載入價格歷史中...'); + + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + modal.show(); + + ensureCampaignChart() + .then(() => fetch(`/api/history/i-code/${encodeURIComponent(iCode)}?range=${activeCampaignHistoryRange}`)) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) + .then(payload => { + const points = payload.data || []; + if (payload.range_label) { + subtitle.textContent = `商品 ID ${iCode} · ${payload.range_label}真實價格紀錄`; + } + + if (!Array.isArray(points) || points.length === 0) { + setCampaignHistoryChartState('目前沒有可顯示的歷史價格紀錄。'); + return; + } + + setCampaignHistoryChartState('', true); + const ctx = canvas.getContext('2d'); + const gradient = ctx.createLinearGradient(0, 0, 0, 380); + gradient.addColorStop(0, 'rgba(190, 106, 45, 0.26)'); + gradient.addColorStop(1, 'rgba(190, 106, 45, 0.04)'); + + campaignPriceChartInstance = new Chart(ctx, { + type: 'line', + data: { + labels: points.map(point => point.t), + datasets: [{ + label: '價格', + data: points.map(point => point.p), + borderColor: '#be6a2d', + backgroundColor: gradient, + borderWidth: 3, + fill: true, + tension: 0.35, + pointRadius: 3, + pointHoverRadius: 7, + pointBackgroundColor: '#be6a2d', + pointBorderColor: '#fff', + pointBorderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: 'rgba(55, 45, 35, 0.94)', + titleColor: '#faf7f0', + bodyColor: '#faf7f0', + borderColor: '#be6a2d', + borderWidth: 1, + displayColors: false, + padding: 12, + callbacks: { + label: context => '價格 ' + formatCampaignPriceTick(context.parsed.y) + } + } + }, + scales: { + y: { + beginAtZero: false, + grid: { + color: 'rgba(71, 61, 49, 0.08)' + }, + ticks: { + color: '#7f715f', + callback: formatCampaignPriceTick + } + }, + x: { + grid: { display: false }, + ticks: { + color: '#9b8a77', + maxRotation: 0, + autoSkip: true, + maxTicksLimit: 8 + } + } + } + } + }); + }) + .catch(error => { + console.error('活動商品價格歷史載入失敗:', error); + setCampaignHistoryChartState('價格歷史載入失敗,請稍後再試。'); + }); + } + + function applyCampaignFilter(card, filter) { + const rows = Array.from(card.querySelectorAll('[data-campaign-row]')); + const empty = card.querySelector('.campaign-filter-empty'); + const counter = card.querySelector('[data-campaign-visible-count]'); + let visibleCount = 0; + + rows.forEach(row => { + const matches = filter === 'all' || row.dataset.campaignFilter === filter; + row.classList.toggle('is-hidden', !matches); + if (matches) visibleCount += 1; + }); + + if (empty) { + empty.classList.toggle('is-hidden', visibleCount !== 0); + } + + if (counter) { + counter.textContent = `${visibleCount.toLocaleString()} 筆`; + } + } + + document.querySelectorAll('.campaign-table-card').forEach(card => { + card.querySelectorAll('[data-campaign-filter]').forEach(button => { + button.addEventListener('click', () => { + card.querySelectorAll('[data-campaign-filter]').forEach(item => { + item.classList.toggle('is-active', item === button); + }); + applyCampaignFilter(card, button.dataset.campaignFilter); + }); + }); + }); + + document.querySelectorAll('[data-campaign-copy]').forEach(button => { + button.addEventListener('click', event => { + event.stopPropagation(); + copyCampaignProductId(button.dataset.campaignCopy, button); + }); + }); + + document.querySelectorAll('[data-campaign-history-trigger]').forEach(button => { + button.addEventListener('click', event => { + event.stopPropagation(); + showCampaignHistory(button.dataset.iCode, button.dataset.productName); + }); + }); + + document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(element => { + bootstrap.Tooltip.getOrCreateInstance(element); + }); + + document.querySelectorAll('[data-campaign-history-range]').forEach(button => { + button.addEventListener('click', () => { + if (!currentCampaignICode) return; + showCampaignHistory(currentCampaignICode, currentCampaignProductName, button.dataset.campaignHistoryRange); + }); + }); + + function triggerEdmTask() { + if (confirm('確定要手動執行 EDM 爬蟲嗎?')) { + fetch('/api/run_edm_task', { + method: 'POST', + headers: { 'X-CSRFToken': getCSRFToken() } + }) + .then(response => response.json()) + .then(data => alert(data.message)) + .catch(error => alert('錯誤: ' + error)); + } + } + + function triggerFestivalTask() { + if (confirm('確定要手動執行節慶活動爬蟲嗎?')) { + fetch('/api/run_festival_task', { + method: 'POST', + headers: { 'X-CSRFToken': getCSRFToken() } + }) + .then(response => response.json()) + .then(data => alert(data.message)) + .catch(error => alert('錯誤: ' + error)); + } + } + + function triggerNotification() { + if (confirm('確定要發送 EDM 活動通知嗎?')) { + fetch('/api/trigger_edm_notification', { + method: 'POST', + headers: { 'X-CSRFToken': getCSRFToken() } + }) + .then(response => response.json()) + .then(data => alert(data.message)) + .catch(error => alert('錯誤: ' + error)); + } + } + + function trackMomoLinkClick(event) { + const link = event.target.closest('.momo-tracked-link'); + if (!link) { + return; + } + + const href = link.getAttribute('href') || ''; + const originalHref = link.dataset.momoOriginalUrl || href; + if (!href || href === '#') { + return; + } + + const payload = { + url: originalHref, + page: location.pathname, + source: link.dataset.trackSource || 'unknown', + platform: link.dataset.trackPlatform || 'momo', + product_id: link.dataset.trackProductId || '', + i_code: link.dataset.trackIcode || '', + product_name: link.dataset.trackProductName || '', + label: (link.textContent || '').trim(), + effective_url: href + }; + + const isBlocked = isBlockedMomoUrl(href); + + if (isBlocked) { + console.warn('[EDM Dashboard V2] 嘗試打開 MOMO 404 網址', payload); + event.preventDefault(); + + const fallbackUrl = link.dataset.momoFallbackUrl || getSafeMomoFallbackUrl(link); + if (fallbackUrl && fallbackUrl !== '#' && fallbackUrl !== href) { + link.dataset.momoFallbackUrl = fallbackUrl; + link.setAttribute('href', fallbackUrl); + payload.effective_url = fallbackUrl; + openMomoUrl(link, fallbackUrl); + fetch('/api/track_momo_link', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken() + }, + body: JSON.stringify(payload) + }).catch(() => {}); + return; + } + } + + fetch('/api/track_momo_link', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken() + }, + body: JSON.stringify(payload) + }).catch(() => {}); + } + + function getSafeMomoFallbackUrl(link) { + const iCode = (link.dataset.trackIcode || link.dataset.trackProductId || '').trim(); + if (!isLikelyMomoProductCode(iCode)) { + return '#'; + } + return `https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=${encodeURIComponent(iCode)}`; + } + + function isBlockedMomoUrl(url) { + const lowered = (url || '').toLowerCase(); + if (lowered.includes('EC404.html') || lowered.includes('ec404')) { + return true; + } + try { + const parsed = new URL(url, location.origin); + const path = (parsed.pathname || '').toLowerCase(); + if (!path.includes('goodsdetail')) { + return false; + } + const code = (parsed.searchParams.get('i_code') || '').trim(); + if (code) { + return !isLikelyMomoProductCode(code); + } + return !/\/goodsdetail\/[^/?#]+/i.test(path); + } catch (error) { + if (!/goodsdetail\.jsp/i.test(lowered)) { + return false; + } + const hasCode = /[?&]i_code=([^&#]+)/i.test(lowered); + if (!hasCode) { + return true; + } + const match = /[?&]i_code=([^&#]+)/i.exec(lowered); + const code = match ? (match[1] || '').trim() : ''; + return !isLikelyMomoProductCode(code); + } + } + + function isLikelyMomoProductCode(value) { + const cleaned = (value || '').trim(); + if (!cleaned) { + return false; + } + const lowered = cleaned.toLowerCase(); + if (lowered === 'nan' || lowered === 'none' || lowered === 'null' || lowered === 'undefined') { + return false; + } + if (lowered.startsWith('momo_') || lowered.startsWith('manual_') || lowered.startsWith('pchome_')) { + return false; + } + return /^[A-Za-z0-9_-]{4,}$/.test(cleaned); + } + + function openMomoUrl(link, url) { + if (!url || url === '#') { + return; + } + + const target = (link.getAttribute('target') || '_self').toLowerCase(); + if (target === '_blank') { + window.open(url, '_blank', 'noopener,noreferrer'); + return; + } + + if (target === '_self' || target === '') { + window.location.href = url; + return; + } + + window.open(url, target); + } + + document.addEventListener('click', trackMomoLinkClick);