feat: improve pchome growth UX workbench
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-06-16 12:03:29 +08:00
parent 3c0e558fbe
commit 08532b2987
6 changed files with 621 additions and 4 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.622"
SYSTEM_VERSION = "V10.623"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -1,8 +1,8 @@
# PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth
> **最後更新**: 2026-06-16 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口高可見頁面繁中化守門已建立
> **適用版本**: V10.622
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口高可見頁面繁中化守門與比價/作戰 UI 工作台化已建立
> **適用版本**: V10.623
---
@@ -69,6 +69,7 @@
- V10.620 起 `unit_comparable` 不再一律丟人工確認:若 `build_unit_price_comparison()` 可產生明確容量/數量、MOMO 單位價、PChome 單位價與差距百分比,候選需標為「自動單位價比較」並回傳 `auto_compare_type=unit_price`。此類候選可自動呈現價格壓力,但不得混入舊總價同款比價表,也不得直接寫入正式價差或自動改價;無法產生單位證據時才維持「需人工確認」。
- V10.621 起 `/price_comparison` 的「自動找 MOMO 候選」會把可直接總價比價與自動單位價候選同步到 `external_offers``ingestion_method='targeted_momo_search'`,人工確認候選不得寫入。`external_offers.raw_payload_json.price_basis='unit_price'` 時,作戰清單必須使用 `unit_price_comparison` 的 MOMO / PChome 單位價與 `unit_gap_pct` 判斷價格壓力;不得把 MOMO 組合總價與 PChome 單品總價直接相減。此同步只影響外部價格參考與作戰清單,不寫 `competitor_prices`,也不自動改價。
- V10.622 起任何 `external_offers` 自動同步成功寫入後,必須呼叫 `mark_pchome_growth_cache_stale()` 寫入共享 cache epoch`/api/ai/pchome-growth/opportunities` 讀快取前必須比對 `get_pchome_growth_cache_epoch()`。這是跨 Gunicorn worker 的可見性保護,避免自動候選已進外部價格參考,但 AI 情報頁仍回 120 秒舊作戰清單。
- V10.623 起 `/price_comparison``/ai_intelligence` 不得只靠大段文字說明流程:比價頁第一屏必須有主 KPI、目前卡點、四步流程與結果決策摘要作戰頁第一屏必須有今日任務、可立即處理、待補比價與最新業績日。所有狀態都要由實際 API/前端狀態驅動,讓使用者一眼知道下一步要按哪個動作。
## 零之一、12 Agent 決策信封2026-05-24

View File

@@ -296,3 +296,10 @@
- `sync_legacy_momo_reference_offers()``sync_targeted_momo_candidates_to_external_offers()` 只要成功寫入 `external_offers`,就呼叫 `mark_pchome_growth_cache_stale()`
- `/api/ai/pchome-growth/opportunities` 的 in-memory cache 會記住建立時的 epoch讀快取前若發現共享 epoch 較新,會直接重建,不再讓使用者看到 120 秒舊清單。
- 這讓「自動找 MOMO 候選 → 同步外部價格參考 → AI 情報頁作戰清單」變成同一條即時資料流,減少使用者手動重新整理或等待快取過期。
## 26. 2026-06-16 V10.623 比價與作戰頁工作台化
- 使用者指出前端仍像文字堆疊無法快速知道怎麼操作V10.623 將 `/price_comparison` 第一屏改為主 KPI、目前卡點、四步流程與下一步 CTA。
- `/price_comparison` 的結果區新增決策摘要,先顯示「需檢查售價或活動 / 可主推曝光 / 觀察賣點」三類數字與建議,再往下看明細表。
- `/ai_intelligence` 第一屏新增今日任務摘要,直接顯示今日任務、可立即處理、待補比價與最新業績日;資料來自現有 PChome growth API stats。
- 測試守門新增 `priceDecisionGrid``price-workflow-strip``price-result-summary-grid``growth-executive-strip``renderGrowthExecutiveSummary`,避免頁面退回只有文字說明的狀態。

View File

@@ -239,6 +239,73 @@
gap: 14px;
}
.growth-executive-strip {
display: grid;
grid-template-columns: minmax(0, 1.25fr) repeat(3, minmax(0, 0.75fr));
gap: 10px;
}
.growth-exec-card {
min-height: 118px;
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(255, 255, 255, 0.82);
box-shadow: var(--momo-shadow-soft);
padding: 13px;
}
.growth-exec-card.is-primary {
border-color: rgba(172, 92, 58, 0.22);
background: rgba(242, 178, 90, 0.15);
}
.growth-exec-card.is-ready {
border-color: rgba(42, 134, 96, 0.24);
background: rgba(235, 248, 241, 0.76);
}
.growth-exec-card.is-gap {
border-color: rgba(188, 78, 67, 0.22);
background: rgba(255, 244, 239, 0.78);
}
.growth-exec-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: var(--momo-text-muted);
font-size: 0.74rem;
font-weight: 900;
}
.growth-exec-label i {
color: var(--momo-warm-rust);
}
.growth-exec-value {
margin-top: 11px;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: 1.05rem;
font-weight: 900;
line-height: 1.28;
}
.growth-exec-card:not(.is-primary) .growth-exec-value {
font-family: var(--momo-font-mono);
font-size: 1.72rem;
line-height: 1;
}
.growth-exec-detail {
margin-top: 7px;
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 760;
line-height: 1.4;
}
.ops-flow {
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
@@ -940,6 +1007,7 @@
}
.growth-ops-grid,
.growth-executive-strip,
.offer-dryrun-grid,
.growth-metric-row,
.ops-flow-grid,
@@ -988,6 +1056,41 @@
</div>
</section>
<section class="growth-executive-strip" aria-label="今日任務摘要">
<article class="growth-exec-card is-primary" id="growthExecTaskCard">
<div class="growth-exec-label">
<span>今日任務</span>
<i class="fas fa-location-arrow"></i>
</div>
<div class="growth-exec-value" id="growthExecTask">整理中</div>
<div class="growth-exec-detail" id="growthExecTaskDetail">正在讀取 PChome 業績與 MOMO 外部價格。</div>
</article>
<article class="growth-exec-card is-ready">
<div class="growth-exec-label">
<span>可立即處理</span>
<i class="fas fa-circle-check"></i>
</div>
<div class="growth-exec-value" id="growthExecReady"></div>
<div class="growth-exec-detail">已有可用比價資料</div>
</article>
<article class="growth-exec-card is-gap" id="growthExecGapCard">
<div class="growth-exec-label">
<span>待補比價</span>
<i class="fas fa-link-slash"></i>
</div>
<div class="growth-exec-value" id="growthExecGap"></div>
<div class="growth-exec-detail">有業績但缺外部參考</div>
</article>
<article class="growth-exec-card">
<div class="growth-exec-label">
<span>最新業績日</span>
<i class="fas fa-calendar-day"></i>
</div>
<div class="growth-exec-value" id="growthExecLatestDate"></div>
<div class="growth-exec-detail" id="growthExecLatestDetail">等待資料</div>
</article>
</section>
<!-- ── 今日重點總覽 ── -->
<section class="ops-flow" aria-label="今日重點總覽">
<div class="ops-flow-head">
@@ -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');

View File

@@ -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 %}
<div class="container-fluid py-4 price-tool-page">
<header class="price-tool-head mb-4">
<div class="price-hero-copy">
<h2><i class="fas fa-balance-scale me-2"></i>PChome 商品比價決策台</h2>
<p class="text-muted mb-0">先確認兩邊資料是否齊,再找出 PChome 價格偏高、可主推或需要補資料的商品</p>
<p class="text-muted mb-0">用 PChome 業績商品反查 MOMO先找同款再判斷售價、活動與曝光下一步</p>
</div>
<div class="price-hero-kpis" aria-label="比價核心狀態">
<div class="price-hero-kpi">
<strong id="heroPchomeCount">0</strong>
<span>PChome 商品</span>
</div>
<div class="price-hero-kpi">
<strong id="heroMomoCount">0</strong>
<span>MOMO 可用候選</span>
</div>
<div class="price-hero-kpi">
<strong id="heroDecisionCount">0</strong>
<span>已產生決策</span>
</div>
</div>
</header>
<section class="price-command-grid" aria-label="比價操作總覽">
@@ -410,6 +668,60 @@
</div>
</section>
<section class="price-decision-grid" id="priceDecisionGrid" aria-label="目前卡點與下一步">
<article class="price-decision-card is-active" id="decisionScopeCard">
<div class="price-decision-top">
<span>檢查範圍</span>
<span class="price-decision-icon"><i class="fas fa-magnifying-glass"></i></span>
</div>
<div class="price-decision-value" id="decisionScopeValue">尚未選擇</div>
<div class="price-decision-detail" id="decisionScopeDetail">先選品牌或輸入商品關鍵字。</div>
</article>
<article class="price-decision-card" id="decisionPchomeCard">
<div class="price-decision-top">
<span>PChome</span>
<span class="price-decision-icon"><i class="fas fa-cart-shopping"></i></span>
</div>
<div class="price-decision-value" id="decisionPchomeValue">等待商品</div>
<div class="price-decision-detail" id="decisionPchomeDetail">取得商品後,系統才知道要比哪一批。</div>
</article>
<article class="price-decision-card" id="decisionMomoCard">
<div class="price-decision-top">
<span>MOMO</span>
<span class="price-decision-icon"><i class="fas fa-store"></i></span>
</div>
<div class="price-decision-value" id="decisionMomoValue">等待候選</div>
<div class="price-decision-detail" id="decisionMomoDetail">會自動分成同款、單位價、需確認。</div>
</article>
<article class="price-decision-card" id="decisionResultCard">
<div class="price-decision-top">
<span>比價結果</span>
<span class="price-decision-icon"><i class="fas fa-chart-simple"></i></span>
</div>
<div class="price-decision-value" id="decisionResultValue">尚未判讀</div>
<div class="price-decision-detail" id="decisionResultDetail">結果會分成檢查售價、主推曝光、觀察賣點。</div>
</article>
</section>
<section class="price-workflow-strip" aria-label="比價流程">
<div class="price-workflow-step is-current" id="workflowStepScope">
<span class="price-workflow-index">1</span>
<span><strong>選範圍</strong><span>品牌或關鍵字</span></span>
</div>
<div class="price-workflow-step" id="workflowStepPchome">
<span class="price-workflow-index">2</span>
<span><strong>抓 PChome</strong><span>取得主場商品</span></span>
</div>
<div class="price-workflow-step" id="workflowStepMomo">
<span class="price-workflow-index">3</span>
<span><strong>找 MOMO</strong><span>同款與單位價</span></span>
</div>
<div class="price-workflow-step" id="workflowStepResult">
<span class="price-workflow-index">4</span>
<span><strong>做決策</strong><span>價格與曝光</span></span>
</div>
</section>
<!-- 操作區 -->
<div class="row mb-4">
<!-- 選擇檢查範圍 -->
@@ -517,6 +829,26 @@
<span><i class="fas fa-chart-bar me-1"></i>比價結果判讀</span>
<span id="priceResultSummary">等待比價結果</span>
</div>
<div class="price-result-summary-grid">
<div class="price-result-callout">
<strong id="priceResultHeadline">尚未產生判讀</strong>
<span id="priceResultAdvice">取得 PChome 與 MOMO 商品後,系統會直接整理下一步。</span>
</div>
<div class="price-result-matrix" aria-label="比價決策分佈">
<div class="price-result-matrix-card">
<strong id="priceUrgentMetric">0</strong>
<span>需檢查售價或活動</span>
</div>
<div class="price-result-matrix-card">
<strong id="priceGoodMetric">0</strong>
<span>可主推曝光</span>
</div>
<div class="price-result-matrix-card">
<strong id="priceWatchMetric">0</strong>
<span>觀察賣點</span>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="price-risk-row">
@@ -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);

View File

@@ -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