@@ -1487,6 +1590,7 @@ function renderOpsCommandDashboard(stats, scope = {}) {
setWidth('opsFunnelMappedBar', candidateCount ? (mappedCount / candidateCount) * 100 : 0);
setWidth('opsFunnelNeedsBar', candidateCount ? (needsMapping / candidateCount) * 100 : 0);
renderOpsSourceBars(stats.external_data_source_counts || {}, scope);
+ renderGrowthExecutiveSummary(stats);
}
function renderNextAction(candidateCount, mappedCount, needsMapping) {
@@ -1529,6 +1633,48 @@ function renderNextAction(candidateCount, mappedCount, needsMapping) {
button.onclick = () => scrollToPanel('externalPricePanel');
}
+function renderGrowthExecutiveSummary(stats = {}) {
+ const candidateCount = Number(stats.candidate_count || 0);
+ const mappedCount = Number(stats.mapped_count || 0);
+ const needsMapping = Number(stats.needs_mapping_count || 0);
+ const latestSalesDate = String(stats.latest_sales_date || '').slice(0, 10);
+
+ document.getElementById('growthExecReady').textContent = formatCount(mappedCount);
+ document.getElementById('growthExecGap').textContent = formatCount(needsMapping);
+ document.getElementById('growthExecLatestDate').textContent = latestSalesDate || '—';
+ document.getElementById('growthExecLatestDetail').textContent = latestSalesDate ? '已接到 PChome 業績' : '尚未確認業績日期';
+
+ const task = document.getElementById('growthExecTask');
+ const detail = document.getElementById('growthExecTaskDetail');
+ const gapCard = document.getElementById('growthExecGapCard');
+ if (!task || !detail) return;
+
+ if (!candidateCount) {
+ task.textContent = '先更新 PChome 業績';
+ detail.textContent = '目前還沒有足夠資料,請確認最新業績檔是否已匯入。';
+ gapCard?.classList.remove('is-gap');
+ return;
+ }
+
+ if (needsMapping > 0 && mappedCount === 0) {
+ task.textContent = `先補 ${formatCount(needsMapping)} 件 MOMO 參考`;
+ detail.textContent = '高業績商品還不能比價,先補對應資料才會有可行動建議。';
+ gapCard?.classList.add('is-gap');
+ return;
+ }
+
+ if (needsMapping > mappedCount) {
+ task.textContent = `先補比價,再處理 ${formatCount(mappedCount)} 件`;
+ detail.textContent = '待補比價比可處理商品多,先擴大 MOMO 對應覆蓋率。';
+ gapCard?.classList.add('is-gap');
+ return;
+ }
+
+ task.textContent = '先檢查價格風險';
+ detail.textContent = `已有 ${formatCount(mappedCount)} 件商品可直接處理,先看 PChome 是否被外部低價壓住。`;
+ gapCard?.classList.toggle('is-gap', needsMapping > 0);
+}
+
function renderOpsSourceBars(counts, scope = {}) {
const box = document.getElementById('opsSourceBars');
const totalBox = document.getElementById('opsSourceTotal');
diff --git a/templates/price_comparison.html b/templates/price_comparison.html
index a0e4f85..39f1e2f 100644
--- a/templates/price_comparison.html
+++ b/templates/price_comparison.html
@@ -9,6 +9,10 @@
}
.price-tool-head {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(360px, 0.82fr);
+ gap: 18px;
+ align-items: stretch;
padding: var(--momo-space-4) var(--momo-space-5);
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
@@ -29,6 +33,45 @@
color: var(--momo-text-secondary) !important;
}
+.price-hero-copy {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: 0;
+}
+
+.price-hero-kpis {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.price-hero-kpi {
+ min-height: 92px;
+ padding: 12px;
+ border: 1px solid rgba(42, 37, 32, 0.1);
+ border-radius: var(--momo-radius-md);
+ background: rgba(250, 247, 240, 0.72);
+}
+
+.price-hero-kpi strong {
+ display: block;
+ color: var(--momo-text-primary);
+ font-family: var(--momo-font-mono, monospace);
+ font-size: 1.55rem;
+ font-weight: 900;
+ line-height: 1;
+}
+
+.price-hero-kpi span {
+ display: block;
+ margin-top: 7px;
+ color: var(--momo-text-secondary);
+ font-size: 0.76rem;
+ font-weight: 850;
+ line-height: 1.35;
+}
+
.price-command-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
@@ -87,6 +130,134 @@
padding: 14px;
}
+.price-decision-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: var(--momo-space-4);
+}
+
+.price-decision-card {
+ display: grid;
+ gap: 8px;
+ min-height: 126px;
+ padding: 13px;
+ border: 1px solid var(--momo-border-light);
+ border-radius: var(--momo-radius-md);
+ background: rgba(255, 255, 255, 0.82);
+ box-shadow: var(--momo-shadow-soft);
+}
+
+.price-decision-card.is-active {
+ border-color: rgba(172, 92, 58, 0.34);
+ background: rgba(242, 178, 90, 0.16);
+}
+
+.price-decision-card.is-ready {
+ border-color: rgba(46, 125, 91, 0.24);
+ background: rgba(235, 248, 241, 0.72);
+}
+
+.price-decision-card.is-blocked {
+ border-color: rgba(200, 81, 58, 0.24);
+ background: rgba(255, 244, 239, 0.78);
+}
+
+.price-decision-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ color: var(--momo-text-secondary);
+ font-size: 0.73rem;
+ font-weight: 900;
+}
+
+.price-decision-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 999px;
+ background: rgba(42, 37, 32, 0.08);
+ color: var(--momo-warm-rust);
+}
+
+.price-decision-value {
+ color: var(--momo-text-primary);
+ font-family: var(--momo-font-display);
+ font-size: 0.98rem;
+ font-weight: 900;
+ line-height: 1.3;
+}
+
+.price-decision-detail {
+ color: var(--momo-text-secondary);
+ font-size: 0.76rem;
+ font-weight: 760;
+ line-height: 1.42;
+}
+
+.price-workflow-strip {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: var(--momo-space-4);
+}
+
+.price-workflow-step {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
+ min-height: 70px;
+ padding: 11px;
+ border: 1px solid rgba(42, 37, 32, 0.1);
+ border-radius: var(--momo-radius-md);
+ background: rgba(250, 247, 240, 0.6);
+}
+
+.price-workflow-step.is-current {
+ border-color: rgba(172, 92, 58, 0.34);
+ background: rgba(242, 178, 90, 0.16);
+}
+
+.price-workflow-step.is-done {
+ border-color: rgba(46, 125, 91, 0.24);
+ background: rgba(235, 248, 241, 0.72);
+}
+
+.price-workflow-step strong {
+ display: block;
+ color: var(--momo-text-primary);
+ font-size: 0.82rem;
+ font-weight: 900;
+ line-height: 1.25;
+}
+
+.price-workflow-step span:last-child {
+ display: block;
+ margin-top: 2px;
+ color: var(--momo-text-secondary);
+ font-size: 0.7rem;
+ font-weight: 780;
+}
+
+.price-workflow-index {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 999px;
+ background: rgba(172, 92, 58, 0.13);
+ color: var(--momo-warm-rust);
+ font-family: var(--momo-font-mono, monospace);
+ font-size: 0.78rem;
+ font-weight: 900;
+}
+
.price-panel-title {
display: flex;
align-items: center;
@@ -226,6 +397,68 @@
color: var(--momo-text-primary);
}
+.price-result-summary-grid {
+ display: grid;
+ grid-template-columns: minmax(260px, 0.92fr) minmax(0, 1.35fr);
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.price-result-callout {
+ display: grid;
+ gap: 8px;
+ padding: 14px;
+ border: 1px solid rgba(172, 92, 58, 0.22);
+ border-radius: var(--momo-radius-md);
+ background: rgba(242, 178, 90, 0.14);
+}
+
+.price-result-callout strong {
+ color: var(--momo-text-primary);
+ font-size: 1rem;
+ font-weight: 900;
+ line-height: 1.35;
+}
+
+.price-result-callout span {
+ color: var(--momo-text-secondary);
+ font-size: 0.8rem;
+ font-weight: 760;
+ line-height: 1.45;
+}
+
+.price-result-matrix {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.price-result-matrix-card {
+ min-height: 92px;
+ padding: 11px;
+ border: 1px solid rgba(42, 37, 32, 0.1);
+ border-radius: var(--momo-radius-md);
+ background: rgba(255, 255, 255, 0.72);
+}
+
+.price-result-matrix-card strong {
+ display: block;
+ color: var(--momo-text-primary);
+ font-family: var(--momo-font-mono, monospace);
+ font-size: 1.45rem;
+ font-weight: 900;
+ line-height: 1;
+}
+
+.price-result-matrix-card span {
+ display: block;
+ margin-top: 7px;
+ color: var(--momo-text-secondary);
+ font-size: 0.74rem;
+ font-weight: 850;
+ line-height: 1.35;
+}
+
.price-note {
padding: 10px 12px;
background: var(--momo-info-bg);
@@ -352,9 +585,18 @@
@media (max-width: 760px) {
.price-tool-head {
+ grid-template-columns: 1fr;
padding: var(--momo-space-4);
}
+ .price-hero-kpis,
+ .price-decision-grid,
+ .price-workflow-strip,
+ .price-result-summary-grid,
+ .price-result-matrix {
+ grid-template-columns: 1fr;
+ }
+
.price-command-grid,
.price-next-action {
grid-template-columns: 1fr;
@@ -379,8 +621,24 @@
{% block content %}
+
+
+
+ 檢查範圍
+
+
+ 尚未選擇
+ 先選品牌或輸入商品關鍵字。
+
+
+
+ PChome
+
+
+ 等待商品
+ 取得商品後,系統才知道要比哪一批。
+
+
+
+ MOMO
+
+
+ 等待候選
+ 會自動分成同款、單位價、需確認。
+
+
+
+ 比價結果
+
+
+ 尚未判讀
+ 結果會分成檢查售價、主推曝光、觀察賣點。
+
+
+
+
+
+ 1
+ 選範圍品牌或關鍵字
+
+
+ 2
+ 抓 PChome取得主場商品
+
+
+ 3
+ 找 MOMO同款與單位價
+
+
+ 4
+ 做決策價格與曝光
+
+
+
@@ -517,6 +829,26 @@
比價結果判讀
等待比價結果
+
+
+ 尚未產生判讀
+ 取得 PChome 與 MOMO 商品後,系統會直接整理下一步。
+
+
+
+ 0
+ 需檢查售價或活動
+
+
+ 0
+ 可主推曝光
+
+
+ 0
+ 觀察賣點
+
+
+
@@ -1051,6 +1383,11 @@ La Roche-Posay 安得利防曬液 50ml,920
comparisonResult = null;
document.getElementById('resultSection').style.display = 'none';
setText('priceResultSummary', '等待比價結果');
+ setText('priceResultHeadline', '尚未產生判讀');
+ setText('priceResultAdvice', '取得 PChome 與 MOMO 商品後,系統會直接整理下一步。');
+ setText('priceUrgentMetric', '0');
+ setText('priceGoodMetric', '0');
+ setText('priceWatchMetric', '0');
}
function renderPriceCommandDashboard() {
@@ -1065,7 +1402,11 @@ La Roche-Posay 安得利防曬液 50ml,920
const urgentCount = Number(stats.momo_cheaper_count || 0);
const goodCount = Number(stats.pchome_cheaper_count || 0);
const watchCount = Math.max(0, matchedCount - urgentCount - goodCount);
+ const usableMomoCount = momoCount + unitCount;
+ setText('heroPchomeCount', pchomeCount.toLocaleString());
+ setText('heroMomoCount', usableMomoCount.toLocaleString());
+ setText('heroDecisionCount', matchedCount.toLocaleString());
setText('pricePchomeReadyText', `${pchomeCount} 筆`);
setText(
'priceMomoReadyText',
@@ -1075,6 +1416,8 @@ La Roche-Posay 安得利防曬液 50ml,920
);
setWidth('pricePchomeReadyBar', Math.min(100, pchomeCount));
setWidth('priceMomoReadyBar', Math.min(100, momoCount + unitCount));
+ renderPriceDecisionCards({ keyword, pchomeCount, momoCount, unitCount, reviewCount, matchedCount, urgentCount, goodCount, watchCount });
+ renderPriceWorkflow({ keyword, pchomeCount, momoCount, unitCount, matchedCount });
if (!keyword && !pchomeCount && !momoCount && !unitCount) {
setText('priceReadySummary', '請先選範圍');
@@ -1126,6 +1469,97 @@ La Roche-Posay 安得利防曬液 50ml,920
setNextAction('今天先做:檢查商品賣點與活動位置', '目前價格接近,差異不大,下一步看曝光、文案和活動組合。', '查看比價結果', 'focus-results');
}
+ function renderPriceDecisionCards(state) {
+ const keyword = state.keyword || '';
+ setDecisionCard(
+ 'decisionScopeCard',
+ 'decisionScopeValue',
+ 'decisionScopeDetail',
+ keyword ? `檢查「${keyword}」` : '尚未選擇',
+ keyword ? '範圍已確認,可以取得 PChome 商品。' : '先選品牌或輸入商品關鍵字。',
+ keyword ? 'ready' : 'active'
+ );
+ setDecisionCard(
+ 'decisionPchomeCard',
+ 'decisionPchomeValue',
+ 'decisionPchomeDetail',
+ state.pchomeCount ? `${state.pchomeCount.toLocaleString()} 筆商品` : '等待商品',
+ state.pchomeCount ? '已可用這批商品反查 MOMO。' : '取得商品後,系統才知道要比哪一批。',
+ state.pchomeCount ? 'ready' : (keyword ? 'active' : '')
+ );
+
+ const momoValue = state.momoCount
+ ? `${state.momoCount.toLocaleString()} 筆同款`
+ : state.unitCount
+ ? `${state.unitCount.toLocaleString()} 筆單位價`
+ : state.reviewCount
+ ? `${state.reviewCount.toLocaleString()} 筆待確認`
+ : '等待候選';
+ const momoDetail = state.momoCount
+ ? '同款可直接進入總價比對。'
+ : state.unitCount
+ ? '單品與組合不同,已改用單位價判讀。'
+ : state.reviewCount
+ ? '需要確認單品、組合或容量後再使用。'
+ : '會自動分成同款、單位價、需確認。';
+ setDecisionCard(
+ 'decisionMomoCard',
+ 'decisionMomoValue',
+ 'decisionMomoDetail',
+ momoValue,
+ momoDetail,
+ state.momoCount || state.unitCount ? 'ready' : (state.pchomeCount ? 'active' : '')
+ );
+
+ const resultValue = state.matchedCount
+ ? `${state.matchedCount.toLocaleString()} 筆判讀`
+ : '尚未判讀';
+ let resultDetail = '結果會分成檢查售價、主推曝光、觀察賣點。';
+ let resultState = state.momoCount ? 'active' : '';
+ if (state.matchedCount) {
+ if (state.urgentCount) {
+ resultDetail = `${state.urgentCount.toLocaleString()} 筆需先檢查售價或活動。`;
+ resultState = 'blocked';
+ } else if (state.goodCount) {
+ resultDetail = `${state.goodCount.toLocaleString()} 筆可主推曝光。`;
+ resultState = 'ready';
+ } else {
+ resultDetail = '價格接近,改看賣點、活動位置與庫存。';
+ resultState = 'ready';
+ }
+ }
+ setDecisionCard('decisionResultCard', 'decisionResultValue', 'decisionResultDetail', resultValue, resultDetail, resultState);
+ }
+
+ function setDecisionCard(cardId, valueId, detailId, value, detail, state) {
+ const card = document.getElementById(cardId);
+ if (card) {
+ card.className = 'price-decision-card' + (
+ state === 'ready' ? ' is-ready' :
+ state === 'blocked' ? ' is-blocked' :
+ state === 'active' ? ' is-active' : ''
+ );
+ }
+ setText(valueId, value);
+ setText(detailId, detail);
+ }
+
+ function renderPriceWorkflow(state) {
+ setWorkflowState('workflowStepScope', state.keyword ? 'done' : 'current');
+ setWorkflowState('workflowStepPchome', state.pchomeCount ? 'done' : (state.keyword ? 'current' : ''));
+ setWorkflowState('workflowStepMomo', state.momoCount || state.unitCount ? 'done' : (state.pchomeCount ? 'current' : ''));
+ setWorkflowState('workflowStepResult', state.matchedCount ? 'done' : (state.momoCount ? 'current' : ''));
+ }
+
+ function setWorkflowState(id, state) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ el.className = 'price-workflow-step' + (
+ state === 'done' ? ' is-done' :
+ state === 'current' ? ' is-current' : ''
+ );
+ }
+
function setNextAction(title, reason, label, action) {
setText('priceNextActionTitle', title);
setText('priceNextActionReason', reason);
@@ -1225,7 +1659,23 @@ La Roche-Posay 安得利防曬液 50ml,920
setText('priceUrgentText', `${urgentCount} 筆`);
setText('priceGoodText', `${goodCount} 筆`);
setText('priceWatchText', `${watchCount} 筆`);
+ setText('priceUrgentMetric', urgentCount.toLocaleString());
+ setText('priceGoodMetric', goodCount.toLocaleString());
+ setText('priceWatchMetric', watchCount.toLocaleString());
setText('priceResultSummary', matches.length ? `共找到 ${matches.length} 筆同款` : '尚未找到同款');
+ if (urgentCount > 0) {
+ setText('priceResultHeadline', `先處理 ${urgentCount.toLocaleString()} 筆 PChome 價格偏高商品`);
+ setText('priceResultAdvice', '建議先檢查售價、折扣、組合內容與活動曝光,避免高業績商品被外部低價壓住。');
+ } else if (goodCount > 0) {
+ setText('priceResultHeadline', `可主推 ${goodCount.toLocaleString()} 筆 PChome 價格有利商品`);
+ setText('priceResultAdvice', '建議安排首頁曝光、搜尋關鍵字、活動文案或 EDM,把價格優勢轉成流量。');
+ } else if (matches.length) {
+ setText('priceResultHeadline', '價格差距不大,改看內容與曝光');
+ setText('priceResultAdvice', '下一步檢查商品頁賣點、圖片、庫存與活動位置,找出非價格因素。');
+ } else {
+ setText('priceResultHeadline', '尚未找到可判讀同款');
+ setText('priceResultAdvice', '請改用更精準的商品名稱、型號或容量搜尋,或先檢查 MOMO 候選。');
+ }
const denominator = Math.max(matches.length, 1);
setWidth('priceUrgentBar', (urgentCount / denominator) * 100);
setWidth('priceGoodBar', (goodCount / denominator) * 100);
diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py
index cb00f86..3c115ff 100644
--- a/tests/test_frontend_v2_assets.py
+++ b/tests/test_frontend_v2_assets.py
@@ -445,6 +445,10 @@ def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis():
assert "PChome 業績成長自動化作戰系統" in template
assert "MOMO 外部價格參考" in template
assert "今日重點總覽" in template
+ assert "今日任務摘要" in template
+ assert "growth-executive-strip" in template
+ assert "growthExecTask" in template
+ assert "renderGrowthExecutiveSummary" in template
assert "商品處理進度" in template
assert "外部價格來源" in template
assert "nextActionTitle" in template
@@ -518,6 +522,15 @@ def test_price_comparison_page_is_action_oriented_and_plain_chinese():
assert "{% extends \"ewoooc_base.html\" %}" in template
assert "PChome 商品比價決策台" in template
+ assert "price-hero-kpis" in template
+ assert "priceDecisionGrid" in template
+ assert "檢查範圍" in template
+ assert "比價流程" in template
+ assert "price-workflow-strip" in template
+ assert "price-result-summary-grid" in template
+ assert "priceResultHeadline" in template
+ assert "renderPriceDecisionCards" in template
+ assert "renderPriceWorkflow" in template
assert "今天先做:選擇要檢查的商品範圍" in template
assert "資料準備狀態" in template
assert "priceNextActionButton" in template