diff --git a/app.py b/app.py index 0db805a..5445bf1 100644 --- a/app.py +++ b/app.py @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-05-01 V10.69: Speed up dashboard competitor overview with real cached product data -SYSTEM_VERSION = "V10.69" +# 🚩 2026-05-01 V10.70: Restore campaign operations table signals +SYSTEM_VERSION = "V10.70" # ========================================== # 🔒 SQL Injection 防護函數 diff --git a/config.py b/config.py index ba1c8e9..e0ebc43 100644 --- a/config.py +++ b/config.py @@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.69" +SYSTEM_VERSION = "V10.70" 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 f8bfdff..655994e 100644 --- a/templates/edm_dashboard_v2.html +++ b/templates/edm_dashboard_v2.html @@ -417,7 +417,7 @@ .campaign-table { width: 100%; - min-width: 920px; + min-width: 1080px; border-collapse: collapse; font-size: var(--momo-font-size-sm); } @@ -483,9 +483,29 @@ } .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 { @@ -547,6 +567,43 @@ 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); @@ -801,7 +858,7 @@ - + + @@ -836,8 +894,15 @@ {% endif %}
{{ item.name }} -
ID {{ item.i_code }}
-
+ +
{% if item.status_change == 'NEW' %} NEW {% elif item.status_change == 'PRICE_DOWN' %} @@ -857,10 +922,11 @@
+ {% else %} - {% endfor %} - @@ -965,6 +1064,36 @@ 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(); } @@ -1144,6 +1273,17 @@ }); }); + document.querySelectorAll('[data-campaign-copy]').forEach(button => { + button.addEventListener('click', event => { + event.stopPropagation(); + copyCampaignProductId(button.dataset.campaignCopy, button); + }); + }); + + 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; diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index e72148b..04da9d5 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -75,6 +75,27 @@ def test_campaign_v2_uses_latest_warm_hero_without_fake_data(): assert "假商品" not in template +def test_campaign_v2_product_table_keeps_real_operations_columns(): + template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8") + route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8") + + assert "分類 / 狀態" in template + assert "銷售 / 庫存" in template + assert "追蹤資訊" in template + assert "data-campaign-copy=\"{{ item.i_code }}\"" in template + assert "copyCampaignProductId" in template + assert "campaign-change-pct" in template + assert "item.total_sold" in template + assert "item.days_on_shelf" in template + assert "item.qty_history" in template + assert "當日銷售歷程" in template + assert "item.crawled_at.strftime('%m/%d %H:%M')" in template + assert "data-bs-toggle=\"tooltip\"" in template + assert "item.total_sold = total_sold_map.get(item.i_code, 0)" in route_source + assert "item.days_on_shelf = days_on_shelf_map.get(item.i_code, 1)" in route_source + assert "item.qty_history = history_map.get((item.i_code, item.time_slot), [])" in route_source + + def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8") dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
分類分類 / 狀態 {% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else 'desc' %} 商品資訊 @@ -813,11 +870,12 @@ {% if current_promo_page == 'edm' %} {% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else 'desc' %} - 倒數組數 + 銷售 / 庫存 {% else %} 狀態 {% endif %} 追蹤資訊
{% 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(1) %} {% if diff < 0 %} -
▼ {{ (diff|abs) | number_format }}
+
▼ {{ (diff|abs) | number_format }}{{ percent }}%
{% else %} -
▲ {{ diff | number_format }}
+
▲ {{ diff | number_format }}{{ percent }}%
{% endif %}
${{ item.previous_price | number_format }}
{% if current_promo_page == 'edm' %} - {% if item.remain_qty is not none %} - 剩 {{ item.remain_qty | number_format }} 組 - {% else %} - -- - {% endif %} +
+ {% if item.remain_qty is not none %} +
剩 {{ item.remain_qty | number_format }} 組
+ {% else %} +
--
+ {% endif %} +
已售 {{ item.total_sold | default(0) | number_format }} 組
+ {% if item.qty_history and item.qty_history|length > 1 %} + {% set tooltip_content %} +
+ 當日銷售歷程
+ {% for h in item.qty_history %} + {{ h.time }}   剩 {{ h.qty }} 組
+ {% endfor %} +
+ {% endset %} + + {% endif %} +
{% else %} {{ item.status_change or '活動中' }} {% endif %}
+
+
上架 {{ item.days_on_shelf | default(1) }} 天
+
時段 {{ item.time_slot or slot }}
+
+ {% if item.crawled_at %} + {{ item.crawled_at.strftime('%m/%d %H:%M') }} + {% else %} + -- + {% endif %} +
+
+
+
此時段目前沒有商品資料