Some checks failed
CD Pipeline / deploy (push) Has been cancelled
- normalize URLs at write time (scheduler crawlers, routes) to drop javascript:/EC404/placeholder i_code (momo_/manual_/pchome_) - add global click+auxclick guard in base.html and ewoooc_base.html that intercepts blocked MOMO URLs and redirects to safe i_code URL - per-page dashboards reuse the same isLikelyMomoIcode validation - /api/track_momo_link records blocked events for diagnosis - ship sanitize_momo_urls.py to clean existing polluted DB rows Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
569 lines
29 KiB
HTML
569 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||
<title>MOMO 限時搶購</title>
|
||
<meta http-equiv="refresh" content="300">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<style>
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
padding-top: 70px;
|
||
}
|
||
|
||
.navbar-dark.bg-primary {
|
||
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
|
||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||
}
|
||
|
||
.navbar-dark .navbar-brand {
|
||
color: #ffffff !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
font-weight: 500;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link:hover {
|
||
color: #ffffff !important;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link.active {
|
||
color: #ffffff !important;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar-dark .navbar-text {
|
||
color: rgba(255, 255, 255, 0.8) !important;
|
||
}
|
||
|
||
.navbar {
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
|
||
}
|
||
|
||
/* 統一使用 dashboard 的柔和色調與樣式 */
|
||
.bg-success-soft { background-color: rgba(25, 135, 84, 0.1) !important; }
|
||
.bg-secondary-soft { background-color: rgba(108, 117, 125, 0.15) !important; }
|
||
.bg-danger-soft { background-color: rgba(220, 53, 69, 0.1) !important; }
|
||
|
||
.table-container { background: white; border-radius: .5rem; box-shadow: 0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2); padding: 1.25rem; margin-bottom: 1.5rem; }
|
||
|
||
.nav-pills .nav-link.active { background-color: #0d6efd; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||
.nav-pills .nav-link { color: #495057; font-weight: 500; }
|
||
|
||
.product-link { text-decoration: none; color: #212529; font-weight: 600; display: block; line-height: 1.4; margin-bottom: 4px; }
|
||
.product-link:hover { color: #0d6efd; }
|
||
|
||
.product-thumb { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; border: 1px solid #dee2e6; margin-right: 12px; }
|
||
|
||
.price-up { color: #dc3545; font-weight: bold; }
|
||
.price-down { color: #198754; font-weight: bold; }
|
||
.price-current { font-weight: 700; font-size: 1.1em; }
|
||
.price-old { text-decoration: line-through; color: #999; font-size: 0.9em; margin-right: 4px; }
|
||
|
||
.table th { font-weight: 600; color: #495057; border-bottom-width: 1px; }
|
||
.badge-status { font-size: 0.75em; vertical-align: middle; margin-left: 6px; }
|
||
.cursor-pointer { cursor: pointer; }
|
||
/* Custom Dark Gray Navbar */
|
||
.navbar.bg-custom-dark {
|
||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-brand {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link {
|
||
color: rgba(255, 255, 255, 0.85);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-text {
|
||
color: rgba(255, 255, 255, 0.75);
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body class="bg-body-tertiary">
|
||
{% include 'components/_navbar.html' %}
|
||
|
||
<div class="container mb-5">
|
||
<!-- V-New: 動態頁籤導覽 -->
|
||
<div class="sub-nav-tabs mb-3">
|
||
{% for page in promo_pages %}
|
||
<a href="{{ page.url }}" class="btn {% if page.id == current_promo_page %}btn-primary{% else %}btn-outline-secondary{% endif %}">
|
||
{{ page.name }}
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-md-8">
|
||
{# V-New: 使用動態頁面標題 #}
|
||
<h2>🔥 {{ page_title }}</h2>
|
||
<p class="text-muted">
|
||
活動時間: {{ activity_time }} |
|
||
最後更新: {{ last_update }} |
|
||
商品總數: {{ total_edm_products }}
|
||
</p>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
{# V-New: 根據不同頁面顯示不同按鈕 #}
|
||
{% if current_promo_page == 'edm' %}
|
||
<button class="btn btn-outline-primary" onclick="triggerEdmTask()">🔄 手動更新</button>
|
||
<button class="btn btn-outline-success" onclick="triggerNotification()">📢 發送通知</button>
|
||
{% elif current_promo_page == 'festival' %}
|
||
<button class="btn btn-outline-primary" onclick="triggerFestivalTask()">🔄 手動更新</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{# V-New: 根據頁面類型讀取對應的排程統計 #}
|
||
{% set task_key = 'festival_task' if current_promo_page == 'festival' else 'edm_task' %}
|
||
{% set edm_stats_list = scheduler_stats.get(task_key, []) %}
|
||
{% if edm_stats_list %}
|
||
{% set latest_run = edm_stats_list[0] %}
|
||
<div class="alert alert-info small p-2 mb-3">
|
||
<i class="fas fa-info-circle me-1"></i>
|
||
<strong>排程統計:</strong>
|
||
上次執行於 {{ latest_run.last_run }},
|
||
共記錄 {{ latest_run.get('changed_records', 0) }} 筆異動。
|
||
狀態:
|
||
{% if latest_run.status == 'Success' %}
|
||
<span class="badge bg-success">成功</span>
|
||
{% else %}
|
||
<span class="badge bg-danger" title="{{ latest_run.get('error', '未知錯誤') }}">失敗</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 時段頁籤 -->
|
||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||
{% for slot, stats in slot_stats.items() %}
|
||
{% set slot_id = slugify(slot) %}
|
||
{% set pane_id = "pills-" ~ slot_id %}
|
||
{% set tab_id = pane_id ~ "-tab" %}
|
||
{% set target_selector = "#" ~ pane_id %}
|
||
{% set is_selected = 'true' if slot == active_tab else 'false' %}
|
||
{% set active_class = 'active' if slot == active_tab else '' %}
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link {{ active_class }} px-3 py-2"
|
||
id="{{ tab_id }}"
|
||
data-bs-toggle="pill"
|
||
data-bs-target="{{ target_selector }}"
|
||
type="button"
|
||
role="tab"
|
||
aria-controls="{{ pane_id }}"
|
||
aria-selected="{{ is_selected }}">
|
||
{{ slot }}
|
||
{# V-New: 改用 on_shelf 數量顯示,更直觀 #}
|
||
<span class="badge bg-white text-primary rounded-pill ms-2 border">{{ stats.on_shelf }}</span>
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
|
||
<div class="tab-content" id="pills-tabContent">
|
||
{% for slot, stats in slot_stats.items() %}
|
||
{% set slot_id = slugify(slot) %}
|
||
{% set pane_id = "pills-" ~ slot_id %}
|
||
{% set items = grouped_items.get(slot, []) %}
|
||
<div class="tab-pane fade {% if slot == active_tab %}show active{% endif %}"
|
||
id="{{ pane_id }}" role="tabpanel">
|
||
|
||
<div class="table-container">
|
||
<!-- 該時段統計 -->
|
||
<div class="d-flex align-items-center mb-3 pb-2 border-bottom">
|
||
<h5 class="mb-0 me-3">商品列表 ({{ items|length }}筆)</h5>
|
||
<div class="small">
|
||
<span class="badge bg-primary me-1">新品: {{ stats['new'] }}</span>
|
||
<span class="badge bg-danger me-1">漲價: {{ stats['up'] }}</span>
|
||
<span class="badge bg-success me-1">降價: {{ stats['down'] }}</span>
|
||
<span class="badge bg-secondary">下架: {{ stats.get('delisted_last_run', 0) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{% set current_endpoint = 'edm.' ~ current_promo_page ~ '_dashboard' %}
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle">
|
||
<thead class="table-light">
|
||
<tr class="text-nowrap">
|
||
<th style="width: 10%;">分類</th>
|
||
<th style="width: 50%;">
|
||
{% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else ('default' if current_sort == 'name' and current_order == 'asc' else 'desc') %}
|
||
<a href="{{ url_for(current_endpoint, sort_by='name' if next_order_name != 'default' else 'default', order=next_order_name) }}" class="text-decoration-none text-muted">
|
||
商品資訊 {% if current_sort == 'name' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
|
||
</a>
|
||
</th>
|
||
<th style="width: 20%;" class="text-end">
|
||
{% set next_order_price = 'asc' if current_sort == 'price' and current_order == 'desc' else ('default' if current_sort == 'price' and current_order == 'asc' else 'desc') %}
|
||
<a href="{{ url_for(current_endpoint, sort_by='price' if next_order_price != 'default' else 'default', order=next_order_price) }}" class="text-decoration-none text-muted">
|
||
價格 {% if current_sort == 'price' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
|
||
</a>
|
||
</th>
|
||
<th style="width: 20%;" class="text-center">
|
||
{% if current_promo_page == 'edm' %}
|
||
{% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else ('default' if current_sort == 'remain_qty' and current_order == 'asc' else 'desc') %}
|
||
<a href="{{ url_for(current_endpoint, sort_by='remain_qty' if next_order_qty != 'default' else 'default', order=next_order_qty) }}" class="text-decoration-none text-muted">
|
||
倒數組數 {% if current_sort == 'remain_qty' %}<i class="fas fa-sort-{{ 'down' if current_order == 'desc' else 'up' }}"></i>{% else %}<i class="fas fa-sort text-muted opacity-25"></i>{% endif %}
|
||
</a>
|
||
{% else %}
|
||
狀態
|
||
{% endif %}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for item in items %}
|
||
<tr>
|
||
<td>
|
||
{% if item.main_category %}
|
||
{% set badge_attr = 'style="background-color: ' ~ item.category_color ~ '; color: #333;"' %}
|
||
<span class="badge" {{ badge_attr | safe }}>{{ item.main_category }}</span>
|
||
{% else %}
|
||
<span class="badge bg-light text-muted">未分類</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div class="d-flex align-items-center">
|
||
<div class="flex-shrink-0">
|
||
{% if item.image_url %}
|
||
<img src="{{ item.image_url }}" class="product-thumb" alt="商品圖" loading="lazy" referrerpolicy="no-referrer">
|
||
{% else %}
|
||
<div class="product-thumb d-flex align-items-center justify-content-center bg-light text-muted small">無圖</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="flex-grow-1">
|
||
<a href="{{ item.safe_product_url or '#' }}" target="_blank" class="product-link momo-tracked-link" title="{{ item.name }}" data-momo-original-url="{{ item.safe_product_url or '#' }}"
|
||
data-track-platform="momo"
|
||
data-track-source="edm-dashboard-table"
|
||
data-track-product-id="{{ item.i_code }}"
|
||
data-track-icode="{{ item.i_code }}"
|
||
data-track-product-name="{{ item.name|e }}">
|
||
{{ item.name }}
|
||
</a>
|
||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||
<small class="text-muted fw-bold cursor-pointer" data-icode="{{ item.i_code }}" onclick="copyToClipboard(event, this.dataset.icode, this)" title="點擊複製品號">
|
||
ID: {{ item.i_code }} <i class="far fa-copy ms-1"></i>
|
||
</small>
|
||
{% if item.status_change == 'NEW' %}
|
||
<span class="badge bg-primary badge-status">NEW</span>
|
||
{% elif item.status_change == 'PRICE_DOWN' %}
|
||
<span class="badge bg-success-soft text-success badge-status">降價</span>
|
||
{% elif item.status_change == 'PRICE_UP' %}
|
||
<span class="badge bg-danger-soft text-danger badge-status">漲價</span>
|
||
{% elif item.status_change == 'DELISTED' or item.status_change == 'SLOT_END' %}
|
||
<span class="badge bg-secondary-soft text-secondary badge-status">下架</span>
|
||
{% endif %}
|
||
|
||
{% if item.discount_text %}
|
||
<span class="badge bg-danger bg-opacity-75 badge-status">{{ item.discount_text }}</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="text-end">
|
||
{% if item.previous_price and item.price and item.previous_price != item.price %}
|
||
{% set diff = item.price - item.previous_price %}
|
||
{% set percent = ((diff|abs) / item.previous_price * 100) | round | int %}
|
||
|
||
{% if diff < 0 %}
|
||
<div>
|
||
<span class="badge bg-success-soft text-success mb-1">▼ {{ (diff|abs) | number_format }} ({{ percent }}%)</span><br>
|
||
<span class="price-old text-muted">${{ item.previous_price | number_format }}</span>
|
||
<span class="price-current text-success">${{ item.price | number_format }}</span>
|
||
</div>
|
||
{% else %}
|
||
<div>
|
||
<span class="badge bg-danger-soft text-danger mb-1">▲ {{ diff | number_format }} ({{ percent }}%)</span><br>
|
||
<span class="price-old text-muted">${{ item.previous_price | number_format }}</span>
|
||
<span class="price-current text-danger">${{ item.price | number_format }}</span>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<span class="price-current text-dark">${{ (item.price | number_format) if item.price is not none else 'N/A' }}</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="text-center">
|
||
{% if current_promo_page == 'edm' %}
|
||
{% set tooltip_content %}
|
||
<div class='text-start p-1'>
|
||
<strong>📈 當日銷售歷程:</strong><br>
|
||
{% for h in item.qty_history %}
|
||
<small>{{ h.time }}</small> 剩 {{ h.qty }} 組<br>
|
||
{% endfor %}
|
||
</div>
|
||
{% endset %}
|
||
<div {% if item.qty_history and item.qty_history|length > 1 %}
|
||
data-bs-toggle="tooltip"
|
||
data-bs-html="true"
|
||
title="{{ tooltip_content }}"
|
||
{% endif %}>
|
||
{% if item.remain_qty is not none %}
|
||
<span class="badge bg-warning text-dark border border-warning">🔥 {{ item.remain_qty | number_format }}組</span>
|
||
{% else %}
|
||
<span class="text-muted small">-</span>
|
||
{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<span class="badge bg-light text-dark border">活動中</span>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// V-New: 初始化 Bootstrap Tooltips
|
||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||
})
|
||
|
||
// Helper function to get CSRF token
|
||
function getCSRFToken() {
|
||
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||
}
|
||
|
||
function triggerEdmTask() {
|
||
if(confirm('確定要手動執行 EDM 爬蟲嗎?')) {
|
||
fetch('/api/run_edm_task', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCSRFToken()
|
||
}
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => alert(data.message))
|
||
.catch(e => alert('錯誤: ' + e));
|
||
}
|
||
}
|
||
function triggerNotification() {
|
||
if(confirm('確定要發送比價通知嗎?')) {
|
||
fetch('/api/trigger_edm_notification', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCSRFToken()
|
||
}
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => alert(data.message))
|
||
.catch(e => alert('錯誤: ' + e));
|
||
}
|
||
}
|
||
|
||
// V-New: 觸發 Festival 爬蟲
|
||
function triggerFestivalTask() {
|
||
if(confirm('確定要手動執行「1.1狂歡購物節」爬蟲嗎?')) {
|
||
fetch('/api/run_festival_task', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': getCSRFToken()
|
||
}
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => alert(data.message))
|
||
.catch(e => alert('錯誤: ' + e));
|
||
}
|
||
}
|
||
|
||
// V-New: 複製品號功能
|
||
function copyToClipboard(event, text, element) {
|
||
event.stopPropagation();
|
||
|
||
const showFeedback = () => {
|
||
const originalHtml = element.innerHTML;
|
||
element.innerHTML = '已複製 <i class="fas fa-check"></i>';
|
||
element.classList.remove('text-muted');
|
||
element.classList.add('text-success');
|
||
setTimeout(() => {
|
||
element.innerHTML = originalHtml;
|
||
element.classList.remove('text-success');
|
||
element.classList.add('text-muted');
|
||
}, 1500);
|
||
};
|
||
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(text).then(showFeedback);
|
||
} else {
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = text;
|
||
textArea.style.position = "fixed";
|
||
textArea.style.left = "-9999px";
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
showFeedback();
|
||
} catch (err) { console.error('複製失敗', err); }
|
||
document.body.removeChild(textArea);
|
||
}
|
||
}
|
||
|
||
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] 嘗試打開 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 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);
|
||
|
||
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);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|