feat: add growth strategy split cards
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.642"
|
||||
SYSTEM_VERSION = "V10.643"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
- V10.640 起 `/ai_intelligence` 必須提供 MOMO 待確認候選操作佇列;使用者可直接確認同款或排除候選。確認後 `external_offers` 會轉為 `verified/verified` 並進入作戰清單,排除後轉為 `rejected/rejected`,兩者都必須清掉 PChome 成長作戰清單快取。
|
||||
- V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字;第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選,下一步必須顯示「確認候選」並跳到候選面板,不得再只顯示「補齊比價」。
|
||||
- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。
|
||||
- V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要,至少包含價格壓力、價格優勢、待確認、缺比價四類;每一類需顯示件數、近 7 天業績與比例條,且可點擊切換明細。舊 KPI 卡也不得是靜態數字,需可導向全部商品、可處理商品、高風險比價或處理紀錄。
|
||||
|
||||
## 零之一、12 Agent 決策信封(2026-05-24)
|
||||
|
||||
|
||||
@@ -300,7 +300,8 @@
|
||||
|
||||
.growth-exec-card.is-clickable,
|
||||
.ops-dashboard-tile.is-clickable,
|
||||
.growth-metric.is-clickable {
|
||||
.growth-metric.is-clickable,
|
||||
#kpiRow .card.is-clickable {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
@@ -310,7 +311,9 @@
|
||||
.ops-dashboard-tile.is-clickable:hover,
|
||||
.ops-dashboard-tile.is-clickable:focus,
|
||||
.growth-metric.is-clickable:hover,
|
||||
.growth-metric.is-clickable:focus {
|
||||
.growth-metric.is-clickable:focus,
|
||||
#kpiRow .card.is-clickable:hover,
|
||||
#kpiRow .card.is-clickable:focus {
|
||||
border-color: rgba(172, 92, 58, 0.34);
|
||||
box-shadow: 0 0 0 3px rgba(172, 92, 58, 0.1), var(--momo-shadow-soft);
|
||||
outline: none;
|
||||
@@ -687,6 +690,103 @@
|
||||
color: var(--momo-text-strong);
|
||||
}
|
||||
|
||||
.growth-strategy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.growth-strategy-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-height: 104px;
|
||||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||||
border-radius: 8px;
|
||||
background: rgba(250, 247, 240, 0.7);
|
||||
color: var(--momo-text-strong);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.growth-strategy-card:hover,
|
||||
.growth-strategy-card:focus,
|
||||
.growth-strategy-card.is-active {
|
||||
border-color: rgba(172, 92, 58, 0.36);
|
||||
box-shadow: 0 0 0 3px rgba(172, 92, 58, 0.1), var(--momo-shadow-soft);
|
||||
outline: 0;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-risk {
|
||||
background: rgba(255, 244, 239, 0.9);
|
||||
border-color: rgba(185, 79, 58, 0.22);
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-advantage {
|
||||
background: rgba(238, 249, 241, 0.92);
|
||||
border-color: rgba(47, 143, 102, 0.22);
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-review {
|
||||
background: rgba(255, 249, 236, 0.92);
|
||||
border-color: rgba(216, 161, 58, 0.24);
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-needs {
|
||||
background: rgba(250, 247, 240, 0.92);
|
||||
border-color: rgba(42, 37, 32, 0.12);
|
||||
}
|
||||
|
||||
.growth-strategy-label {
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.growth-strategy-value {
|
||||
color: var(--momo-text-strong);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: 1.55rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.growth-strategy-note {
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 760;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.growth-strategy-track {
|
||||
overflow: hidden;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(42, 37, 32, 0.08);
|
||||
}
|
||||
|
||||
.growth-strategy-bar {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--momo-warm-caramel);
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-risk .growth-strategy-bar {
|
||||
background: #b94f3a;
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-advantage .growth-strategy-bar {
|
||||
background: #2f8f66;
|
||||
}
|
||||
|
||||
.growth-strategy-card.is-review .growth-strategy-bar {
|
||||
background: #d8a13a;
|
||||
}
|
||||
|
||||
.growth-detail-result {
|
||||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||||
border-radius: 8px;
|
||||
@@ -1210,6 +1310,7 @@
|
||||
.growth-executive-strip,
|
||||
.offer-dryrun-grid,
|
||||
.growth-metric-row,
|
||||
.growth-strategy-grid,
|
||||
.ops-flow-grid,
|
||||
.offer-dryrun-summary,
|
||||
.price-risk-board {
|
||||
@@ -1391,6 +1492,32 @@
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="source" onclick="showGrowthDetail('source')">有外部價</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="growth-strategy-grid" id="growthStrategyGrid" aria-label="商品策略分流">
|
||||
<button type="button" class="growth-strategy-card is-risk" onclick="showGrowthDetail('risk')">
|
||||
<span class="growth-strategy-label">價格壓力</span>
|
||||
<span class="growth-strategy-value">—</span>
|
||||
<span class="growth-strategy-note">檢查售價、券或組合</span>
|
||||
<span class="growth-strategy-track"><span class="growth-strategy-bar"></span></span>
|
||||
</button>
|
||||
<button type="button" class="growth-strategy-card is-advantage" onclick="showGrowthDetail('advantage')">
|
||||
<span class="growth-strategy-label">價格優勢</span>
|
||||
<span class="growth-strategy-value">—</span>
|
||||
<span class="growth-strategy-note">放大曝光與主推位置</span>
|
||||
<span class="growth-strategy-track"><span class="growth-strategy-bar"></span></span>
|
||||
</button>
|
||||
<button type="button" class="growth-strategy-card is-review" onclick="showGrowthDetail('review')">
|
||||
<span class="growth-strategy-label">待確認</span>
|
||||
<span class="growth-strategy-value">—</span>
|
||||
<span class="growth-strategy-note">先確認同款再判斷價格</span>
|
||||
<span class="growth-strategy-track"><span class="growth-strategy-bar"></span></span>
|
||||
</button>
|
||||
<button type="button" class="growth-strategy-card is-needs" onclick="showGrowthDetail('needs')">
|
||||
<span class="growth-strategy-label">缺比價</span>
|
||||
<span class="growth-strategy-value">—</span>
|
||||
<span class="growth-strategy-note">補抓 MOMO 候選</span>
|
||||
<span class="growth-strategy-track"><span class="growth-strategy-bar"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="growth-detail-result" id="growthDrilldownResult">
|
||||
<div class="text-center py-4 text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>整理商品明細中...
|
||||
@@ -1493,7 +1620,7 @@
|
||||
<!-- ── KPI 卡片 ── -->
|
||||
<div class="row g-3 mb-4" id="kpiRow">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card border-0 shadow-sm h-100 is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="fs-2 fw-bold text-primary" id="kpiSkus">—</div>
|
||||
<div class="small text-muted mt-1"><i class="fas fa-box me-1"></i>監控商品數</div>
|
||||
@@ -1501,7 +1628,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card border-0 shadow-sm h-100 is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="fs-2 fw-bold text-success" id="kpiCompetitors">—</div>
|
||||
<div class="small text-muted mt-1">
|
||||
@@ -1513,7 +1640,7 @@
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<!-- 高風險卡 — 數值來自全量 CTE,非前端截斷的 200 筆 -->
|
||||
<div class="card border-0 shadow-sm h-100" id="kpiHighRiskCard">
|
||||
<div class="card border-0 shadow-sm h-100 is-clickable" id="kpiHighRiskCard" role="button" tabindex="0" onclick="showPriceRiskDetail('HIGH')" onkeydown="handlePriceRiskKey(event, 'HIGH')">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="fs-2 fw-bold text-danger" id="kpiHighRisk">—</div>
|
||||
<div class="small text-muted mt-1">
|
||||
@@ -1523,7 +1650,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card border-0 shadow-sm h-100 is-clickable" role="button" tabindex="0" onclick="scrollToPanel('aiRecsPanel')" onkeydown="handleDrilldownKey(event, 'aiRecsPanel')">
|
||||
<div class="card-body text-center py-3">
|
||||
<div class="fs-2 fw-bold text-info" id="kpiAiRecs">—</div>
|
||||
<div class="small text-muted mt-1"><i class="fas fa-clipboard-check me-1"></i>處理紀錄</div>
|
||||
@@ -1603,7 +1730,7 @@
|
||||
|
||||
<!-- ── 右:最近處理紀錄 ── -->
|
||||
<div class="col-xl-5">
|
||||
<div class="card shadow-sm h-100 ai-panel">
|
||||
<div class="card shadow-sm h-100 ai-panel" id="aiRecsPanel">
|
||||
<div class="card-header py-2 bg-white border-bottom">
|
||||
<span class="ai-panel-title">
|
||||
<i class="fas fa-clipboard-check text-danger me-2"></i>最近處理紀錄
|
||||
@@ -1788,8 +1915,8 @@ function renderKPIs(stats) {
|
||||
// 高風險卡:數值 > 0 加紅底強調
|
||||
document.getElementById('kpiHighRiskCard').className =
|
||||
hr > 0
|
||||
? 'card border-2 border-danger shadow-sm h-100'
|
||||
: 'card border-0 shadow-sm h-100';
|
||||
? 'card border-2 border-danger shadow-sm h-100 is-clickable'
|
||||
: 'card border-0 shadow-sm h-100 is-clickable';
|
||||
}
|
||||
|
||||
function formatMoney(value) {
|
||||
@@ -2248,6 +2375,52 @@ function showGrowthDetail(kind, shouldScroll = true) {
|
||||
if (shouldScroll) scrollToPanel('growthDrilldownPanel');
|
||||
}
|
||||
|
||||
function renderGrowthStrategySummary() {
|
||||
const box = document.getElementById('growthStrategyGrid');
|
||||
if (!box) return;
|
||||
|
||||
const total = Math.max(1, (Array.isArray(latestGrowthRows) ? latestGrowthRows.length : 0));
|
||||
const strategies = [
|
||||
{
|
||||
kind: 'risk',
|
||||
className: 'is-risk',
|
||||
label: '價格壓力',
|
||||
note: '檢查售價、券或組合',
|
||||
},
|
||||
{
|
||||
kind: 'advantage',
|
||||
className: 'is-advantage',
|
||||
label: '價格優勢',
|
||||
note: '放大曝光與主推位置',
|
||||
},
|
||||
{
|
||||
kind: 'review',
|
||||
className: 'is-review',
|
||||
label: '待確認',
|
||||
note: '先確認同款再判斷價格',
|
||||
},
|
||||
{
|
||||
kind: 'needs',
|
||||
className: 'is-needs',
|
||||
label: '缺比價',
|
||||
note: '補抓 MOMO 候選',
|
||||
},
|
||||
];
|
||||
|
||||
box.innerHTML = strategies.map((strategy) => {
|
||||
const rows = growthDetailRows(strategy.kind);
|
||||
const sales = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0);
|
||||
const pct = clampPercent((rows.length / total) * 100);
|
||||
const activeClass = activeGrowthDetailKind === strategy.kind ? ' is-active' : '';
|
||||
return `<button type="button" class="growth-strategy-card ${strategy.className}${activeClass}" onclick="showGrowthDetail('${strategy.kind}')">
|
||||
<span class="growth-strategy-label">${escapeHtml(strategy.label)}</span>
|
||||
<span class="growth-strategy-value">${formatCount(rows.length)}</span>
|
||||
<span class="growth-strategy-note">${escapeHtml(strategy.note)} · ${escapeHtml(formatMoney(sales))}</span>
|
||||
<span class="growth-strategy-track"><span class="growth-strategy-bar" style="width:${pct}%"></span></span>
|
||||
</button>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
const titleBox = document.getElementById('growthDrilldownTitle');
|
||||
const metaBox = document.getElementById('growthDrilldownMeta');
|
||||
@@ -2258,6 +2431,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
document.querySelectorAll('.growth-detail-tab').forEach((tab) => {
|
||||
tab.classList.toggle('is-active', tab.dataset.detailKind === activeGrowthDetailKind);
|
||||
});
|
||||
renderGrowthStrategySummary();
|
||||
|
||||
const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind);
|
||||
const rows = growthDetailRows(activeGrowthDetailKind);
|
||||
|
||||
@@ -452,6 +452,11 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
|
||||
assert "showPriceRiskDetail" in template
|
||||
assert "handlePriceRiskKey" in template
|
||||
assert "opportunities?limit=50" in template
|
||||
assert "growthStrategyGrid" in template
|
||||
assert "renderGrowthStrategySummary" in template
|
||||
assert "商品策略分流" in template
|
||||
assert "growth-strategy-card" in template
|
||||
assert "aiRecsPanel" in template
|
||||
assert "scrollToPanel('externalPricePanel')" in template
|
||||
assert "備援資料檢查" in template
|
||||
assert "外部報價預檢" not in template
|
||||
|
||||
Reference in New Issue
Block a user