feat: show price evidence in growth details
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.643"
|
||||
SYSTEM_VERSION = "V10.644"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
- V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字;第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選,下一步必須顯示「確認候選」並跳到候選面板,不得再只顯示「補齊比價」。
|
||||
- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。
|
||||
- V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要,至少包含價格壓力、價格優勢、待確認、缺比價四類;每一類需顯示件數、近 7 天業績與比例條,且可點擊切換明細。舊 KPI 卡也不得是靜態數字,需可導向全部商品、可處理商品、高風險比價或處理紀錄。
|
||||
- V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價;每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據,並保留下一步按鈕。單位價候選需顯示單位價與單位,候選待確認或缺資料則以「待補 / 候選待確認」呈現,不得捏造價格。
|
||||
|
||||
## 零之一、12 Agent 決策信封(2026-05-24)
|
||||
|
||||
|
||||
@@ -798,7 +798,7 @@
|
||||
|
||||
.growth-detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(118px, auto);
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(42, 37, 32, 0.08);
|
||||
padding: 11px 12px;
|
||||
@@ -827,7 +827,61 @@
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
justify-items: end;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.growth-detail-action .table-row-action {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.growth-detail-price-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.growth-price-chip {
|
||||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||||
border-radius: 8px;
|
||||
background: rgba(250, 247, 240, 0.62);
|
||||
padding: 7px 8px;
|
||||
}
|
||||
|
||||
.growth-price-chip.is-risk {
|
||||
border-color: rgba(185, 79, 58, 0.22);
|
||||
background: rgba(255, 244, 239, 0.86);
|
||||
}
|
||||
|
||||
.growth-price-chip.is-good {
|
||||
border-color: rgba(47, 143, 102, 0.2);
|
||||
background: rgba(238, 249, 241, 0.88);
|
||||
}
|
||||
|
||||
.growth-price-label {
|
||||
display: block;
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.growth-price-value {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: var(--momo-text-strong);
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.growth-price-note {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 760;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.growth-source-list {
|
||||
@@ -1326,6 +1380,14 @@
|
||||
.growth-detail-action {
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.growth-detail-action .table-row-action {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.growth-detail-price-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1924,6 +1986,37 @@ function formatMoney(value) {
|
||||
return 'NT$ ' + Math.round(num).toLocaleString();
|
||||
}
|
||||
|
||||
function formatPriceAmount(value, options = {}) {
|
||||
if (value === null || value === undefined || value === '') return '待補';
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num) || num <= 0) return '待補';
|
||||
const decimals = options.decimals ?? (num < 100 ? 2 : 0);
|
||||
return 'NT$ ' + num.toLocaleString('zh-TW', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
});
|
||||
}
|
||||
|
||||
function formatGrowthDetailPrice(price, side) {
|
||||
if (!price) return '待補';
|
||||
const isUnit = price.price_basis === 'unit_price';
|
||||
const unit = price.unit_label ? ` / ${price.unit_label}` : '';
|
||||
if (isUnit) {
|
||||
const value = side === 'pchome' ? price.pchome_unit_price : price.momo_unit_price;
|
||||
return `${formatPriceAmount(value, { decimals: 2 })}${unit}`;
|
||||
}
|
||||
const value = side === 'pchome' ? price.pchome_price : price.momo_price;
|
||||
return formatPriceAmount(value);
|
||||
}
|
||||
|
||||
function formatGapDisplay(gap) {
|
||||
if (gap === null || gap === undefined || !Number.isFinite(Number(gap))) return '待判斷';
|
||||
const num = Number(gap);
|
||||
if (num < 0) return `PChome 貴 ${Math.abs(num).toFixed(1)}%`;
|
||||
if (num > 0) return `PChome 便宜 ${num.toFixed(1)}%`;
|
||||
return '價格接近';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
|
||||
'&': '&',
|
||||
@@ -2457,14 +2550,33 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
? '候選待確認'
|
||||
: gap === null
|
||||
? '缺 MOMO 參考'
|
||||
: gap < 0
|
||||
? `PChome 貴 ${Math.abs(gap).toFixed(1)}%`
|
||||
: gap > 0
|
||||
? `PChome 便宜 ${gap.toFixed(1)}%`
|
||||
: '價格差不多';
|
||||
: formatGapDisplay(gap);
|
||||
const delta = row.sales_delta_pct === null || row.sales_delta_pct === undefined
|
||||
? '前期不足'
|
||||
: `${Number(row.sales_delta_pct).toFixed(1)}%`;
|
||||
const qualityScore = row.data_quality?.score !== null && row.data_quality?.score !== undefined
|
||||
? Math.round(Number(row.data_quality.score || 0))
|
||||
: reviewCandidate?.quality_score !== null && reviewCandidate?.quality_score !== undefined
|
||||
? Math.round(Number(reviewCandidate.quality_score || 0))
|
||||
: price?.match_score
|
||||
? Math.round(Number(price.match_score || 0) * 100)
|
||||
: 0;
|
||||
const pchomeDisplay = price
|
||||
? formatGrowthDetailPrice(price, 'pchome')
|
||||
: reviewCandidate
|
||||
? formatPriceAmount(reviewCandidate.pchome_price)
|
||||
: '待補';
|
||||
const momoDisplay = price
|
||||
? formatGrowthDetailPrice(price, 'momo')
|
||||
: reviewCandidate
|
||||
? formatPriceAmount(reviewCandidate.momo_price)
|
||||
: '待補';
|
||||
const priceBasis = price?.price_basis_label || (reviewCandidate ? '候選價' : '待補資料');
|
||||
const gapChipClass = gap !== null && gap < 0
|
||||
? ' is-risk'
|
||||
: gap !== null && gap > 0
|
||||
? ' is-good'
|
||||
: '';
|
||||
const productKey = escapeHtml(row.pchome_product_id || row.product_name || '');
|
||||
const nextAction = action.code === 'review_external_candidate'
|
||||
? 'review-candidate'
|
||||
@@ -2487,7 +2599,28 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
</p>
|
||||
</div>
|
||||
<div class="growth-detail-action">
|
||||
<span class="growth-action-pill">${escapeHtml(action.label || '待判斷')}</span>
|
||||
<div class="growth-detail-price-grid">
|
||||
<span class="growth-price-chip">
|
||||
<span class="growth-price-label">PChome</span>
|
||||
<span class="growth-price-value">${escapeHtml(pchomeDisplay)}</span>
|
||||
<span class="growth-price-note">${escapeHtml(priceBasis)}</span>
|
||||
</span>
|
||||
<span class="growth-price-chip">
|
||||
<span class="growth-price-label">MOMO</span>
|
||||
<span class="growth-price-value">${escapeHtml(momoDisplay)}</span>
|
||||
<span class="growth-price-note">${escapeHtml(price?.data_source_label || (reviewCandidate ? '候選待確認' : '等待補抓'))}</span>
|
||||
</span>
|
||||
<span class="growth-price-chip${gapChipClass}">
|
||||
<span class="growth-price-label">差距</span>
|
||||
<span class="growth-price-value">${escapeHtml(gapText)}</span>
|
||||
<span class="growth-price-note">${escapeHtml(action.label || '待判斷')}</span>
|
||||
</span>
|
||||
<span class="growth-price-chip">
|
||||
<span class="growth-price-label">可信度</span>
|
||||
<span class="growth-price-value">${qualityScore ? `${qualityScore}%` : '待補'}</span>
|
||||
<span class="growth-price-note">${escapeHtml(row.data_quality?.label || '資料待確認')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" data-growth-action="${nextAction}" data-product-key="${productKey}">${nextLabel}</button>
|
||||
</div>
|
||||
</article>`;
|
||||
|
||||
@@ -457,6 +457,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
|
||||
assert "商品策略分流" in template
|
||||
assert "growth-strategy-card" in template
|
||||
assert "aiRecsPanel" in template
|
||||
assert "growth-detail-price-grid" in template
|
||||
assert "growth-price-chip" in template
|
||||
assert "formatGrowthDetailPrice" in template
|
||||
assert "formatGapDisplay" in template
|
||||
assert "scrollToPanel('externalPricePanel')" in template
|
||||
assert "備援資料檢查" in template
|
||||
assert "外部報價預檢" not in template
|
||||
|
||||
Reference in New Issue
Block a user