feat(campaign): restore operations table signals
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
4
app.py
4
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 防護函數
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<table class="campaign-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分類</th>
|
||||
<th>分類 / 狀態</th>
|
||||
<th>
|
||||
{% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else 'desc' %}
|
||||
<a href="{{ url_for(current_endpoint, sort_by='name', order=next_order_name) }}">商品資訊</a>
|
||||
@@ -813,11 +870,12 @@
|
||||
<th class="text-center">
|
||||
{% if current_promo_page == 'edm' %}
|
||||
{% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else 'desc' %}
|
||||
<a href="{{ url_for(current_endpoint, sort_by='remain_qty', order=next_order_qty) }}">倒數組數</a>
|
||||
<a href="{{ url_for(current_endpoint, sort_by='remain_qty', order=next_order_qty) }}">銷售 / 庫存</a>
|
||||
{% else %}
|
||||
狀態
|
||||
{% endif %}
|
||||
</th>
|
||||
<th>追蹤資訊</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -836,8 +894,15 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<a class="campaign-product-name" href="{{ item.url or '#' }}" target="_blank" rel="noopener noreferrer">{{ item.name }}</a>
|
||||
<div class="campaign-product-id momo-mono">ID {{ item.i_code }}</div>
|
||||
<div class="campaign-stat-badges" style="margin-top:6px;">
|
||||
<button
|
||||
class="campaign-product-id"
|
||||
type="button"
|
||||
data-campaign-copy="{{ item.i_code }}"
|
||||
title="複製 MOMO 商品 ID"
|
||||
>
|
||||
MOMO {{ item.i_code }} <i class="far fa-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="campaign-product-meta">
|
||||
{% if item.status_change == 'NEW' %}
|
||||
<span class="campaign-badge">NEW</span>
|
||||
{% elif item.status_change == 'PRICE_DOWN' %}
|
||||
@@ -857,10 +922,11 @@
|
||||
<td class="text-end momo-mono">
|
||||
{% 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 %}
|
||||
<div class="campaign-change-down">▼ {{ (diff|abs) | number_format }}</div>
|
||||
<div class="campaign-change-down">▼ {{ (diff|abs) | number_format }}<span class="campaign-change-pct">{{ percent }}%</span></div>
|
||||
{% else %}
|
||||
<div class="campaign-change-up">▲ {{ diff | number_format }}</div>
|
||||
<div class="campaign-change-up">▲ {{ diff | number_format }}<span class="campaign-change-pct">{{ percent }}%</span></div>
|
||||
{% endif %}
|
||||
<div><span class="campaign-old-price">${{ item.previous_price | number_format }}</span></div>
|
||||
<button
|
||||
@@ -894,25 +960,58 @@
|
||||
</td>
|
||||
<td class="text-center momo-mono">
|
||||
{% if current_promo_page == 'edm' %}
|
||||
{% if item.remain_qty is not none %}
|
||||
<span class="campaign-badge" style="color:var(--momo-warning-text);background:var(--momo-warning-bg);">剩 {{ item.remain_qty | number_format }} 組</span>
|
||||
{% else %}
|
||||
<span style="color:var(--momo-text-tertiary);">--</span>
|
||||
{% endif %}
|
||||
<div class="campaign-sales-stack">
|
||||
{% if item.remain_qty is not none %}
|
||||
<div class="campaign-sales-main">剩 {{ item.remain_qty | number_format }} 組</div>
|
||||
{% else %}
|
||||
<div class="campaign-sales-main" style="color:var(--momo-text-tertiary);">--</div>
|
||||
{% endif %}
|
||||
<div class="campaign-sales-sub">已售 {{ item.total_sold | default(0) | number_format }} 組</div>
|
||||
{% if item.qty_history and item.qty_history|length > 1 %}
|
||||
{% 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 %}
|
||||
<button
|
||||
class="campaign-tooltip-trigger"
|
||||
type="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-html="true"
|
||||
title="{{ tooltip_content }}"
|
||||
>銷售歷程</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="campaign-badge">{{ item.status_change or '活動中' }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="momo-mono">
|
||||
<div class="campaign-track-stack">
|
||||
<div class="campaign-track-line">上架 {{ item.days_on_shelf | default(1) }} 天</div>
|
||||
<div class="campaign-track-line">時段 {{ item.time_slot or slot }}</div>
|
||||
<div class="campaign-track-line">
|
||||
{% if item.crawled_at %}
|
||||
{{ item.crawled_at.strftime('%m/%d %H:%M') }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<td colspan="5">
|
||||
<div class="campaign-empty">此時段目前沒有商品資料</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="campaign-filter-empty is-hidden">
|
||||
<td colspan="4">
|
||||
<td colspan="5">
|
||||
<div class="campaign-empty">此篩選條件目前沒有商品資料</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -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 = '已複製 <i class="fas fa-check" aria-hidden="true"></i>';
|
||||
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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user