fix: professionalize marketplace product ux

This commit is contained in:
ogt
2026-06-26 12:29:20 +08:00
parent 2888bac597
commit 5327dfda1f
13 changed files with 596 additions and 73 deletions

View File

@@ -58,7 +58,7 @@
align-items: center;
justify-content: flex-end;
gap: 8px;
max-width: 680px;
max-width: 780px;
}
.growth-command-status-pill {
@@ -75,6 +75,53 @@
padding: 7px 12px;
}
.growth-command-action-group {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.growth-command-action-group.is-secondary {
padding-left: 8px;
border-left: 1px solid rgba(42, 37, 32, 0.12);
}
.growth-command-pro .ai-action-btn {
min-height: 38px;
border-width: 1px;
box-shadow: none;
}
.growth-command-pro .ai-action-btn.btn-primary {
background: #2267d5;
border-color: #2267d5;
color: #fff;
}
.growth-command-pro .ai-action-btn.btn-primary:hover,
.growth-command-pro .ai-action-btn.btn-primary:focus {
background: #1d58b9;
border-color: #1d58b9;
}
.growth-command-pro .ai-action-btn.btn-outline-primary,
.growth-command-pro .ai-action-btn.btn-outline-secondary {
background: #fff;
border-color: rgba(71, 82, 97, 0.45);
color: #3e4a59;
}
.growth-command-pro .ai-action-btn.btn-outline-primary:hover,
.growth-command-pro .ai-action-btn.btn-outline-primary:focus,
.growth-command-pro .ai-action-btn.btn-outline-secondary:hover,
.growth-command-pro .ai-action-btn.btn-outline-secondary:focus {
background: rgba(34, 103, 213, 0.08);
border-color: rgba(34, 103, 213, 0.55);
color: #1f4f9f;
}
.growth-command-kpi-grid {
display: grid;
grid-template-columns: 1.28fr repeat(4, minmax(0, 1fr));
@@ -425,6 +472,10 @@
box-shadow: var(--momo-shadow-soft);
}
.ai-intel-legacy-status {
display: none;
}
.ai-intel-hero::after {
content: "";
position: absolute;
@@ -1869,6 +1920,7 @@
.growth-source-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 10px;
}
@@ -2285,6 +2337,21 @@
font-size: 1rem;
}
.review-candidate-thumb.is-missing {
align-content: center;
gap: 2px;
color: #786f63;
font-size: 0.62rem;
font-weight: 900;
line-height: 1.1;
text-align: center;
}
.review-candidate-thumb.is-missing i {
display: block;
font-size: 0.92rem;
}
.review-candidate-thumb img {
width: 100%;
height: 100%;
@@ -2312,6 +2379,24 @@
line-height: 1.25;
}
.review-candidate-store-meta {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 6px;
}
.review-candidate-store-meta span {
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: var(--momo-text-muted);
font-family: var(--momo-font-mono);
font-size: 0.66rem;
font-weight: 900;
padding: 2px 7px;
}
.review-candidate-store a {
white-space: nowrap;
font-size: 0.7rem;
@@ -2605,6 +2690,7 @@
}
.growth-ops-grid,
.growth-source-list,
.review-candidate-panel,
.growth-executive-strip,
.offer-dryrun-grid,
@@ -2746,25 +2832,29 @@
<div class="growth-command-pro-head">
<div>
<h1 class="growth-command-pro-title">PChome 業績成長系統</h1>
<p class="growth-command-pro-subtitle">評估業績、分析價差、決定今天的解法</p>
<p class="growth-command-pro-subtitle">先看今日優先商品,再決定價格、曝光、組合與資料補強</p>
</div>
<div class="growth-command-pro-actions">
<span id="growthCommandStatus" class="growth-command-status-pill">
<i class="fas fa-circle-check"></i>
<span>讀取中</span>
</span>
<button class="btn btn-primary btn-sm ai-action-btn" id="btnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>更新今日建議
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnPickList" data-action="generate-picks" onclick="generatePickList()">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生今日清單
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="btnBackfill" data-action="backfill" onclick="backfillPchomeMatches()">
<i class="fas fa-magnifying-glass-chart me-1"></i>補齊比價資料
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard(); loadGrowthOps(true);">
<i class="fas fa-rotate me-1"></i>重新整理
</button>
<div class="growth-command-action-group" aria-label="今日主要操作">
<button class="btn btn-primary btn-sm ai-action-btn" id="btnPickList" data-action="generate-picks" onclick="generatePickList()">
<i class="fas fa-list-check me-1"></i>產生今日清單
</button>
<button class="btn btn-primary btn-sm ai-action-btn" id="btnBackfill" data-action="backfill" onclick="backfillPchomeMatches()">
<i class="fas fa-link me-1"></i>補齊比價資料
</button>
</div>
<div class="growth-command-action-group is-secondary" aria-label="資料更新操作">
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-sparkles me-1"></i>更新建議
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard(); loadGrowthOps(true);">
<i class="fas fa-rotate me-1"></i>重新整理
</button>
</div>
</div>
</div>
@@ -2898,7 +2988,7 @@
</section>
<!-- ── 頁首 ── -->
<section class="ai-intel-hero">
<section class="ai-intel-hero ai-intel-legacy-status" aria-hidden="true">
<div>
<h1 class="ai-intel-title">
<i class="fas fa-brain"></i>
@@ -2911,14 +3001,14 @@
<span id="lastUpdateBadge" class="ai-status-badge">
<i class="fas fa-sync me-1"></i>載入中...
</span>
<button class="btn btn-outline-danger btn-sm ai-action-btn" id="legacyBtnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>更新今日建議
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="legacyBtnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-sparkles me-1"></i>更新建議
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="legacyBtnPickList" data-action="generate-picks" onclick="generatePickList()" title="依目前業績與比價資料整理商品處理清單">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生今日清單
<i class="fas fa-list-check me-1"></i>產生今日清單
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="legacyBtnBackfill" data-action="backfill" onclick="backfillPchomeMatches()" title="替還不能比價的 PChome 商品尋找 MOMO 參考">
<i class="fas fa-magnifying-glass-chart me-1"></i>補齊比價資料
<i class="fas fa-link me-1"></i>補齊比價資料
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard()" title="重新載入畫面資料">
<i class="fas fa-redo me-1"></i>重新整理
@@ -3653,6 +3743,14 @@ function renderReasonChips(labels) {
return chips.map((label) => `<span class="review-candidate-pill is-review">${escapeHtml(label)}</span>`).join('');
}
function renderProductThumb(imageUrl, altText) {
const safeUrl = safeHttpUrl(imageUrl);
if (!safeUrl) {
return '<span class="review-candidate-thumb is-missing"><i class="fas fa-image"></i><span>待補圖片</span></span>';
}
return `<span class="review-candidate-thumb"><img src="${escapeHtml(safeUrl)}" alt="${escapeHtml(altText || '商品圖')}" loading="lazy" onerror="this.closest('.review-candidate-thumb').outerHTML='<span class=&quot;review-candidate-thumb is-missing&quot;><i class=&quot;fas fa-image&quot;></i><span>待補圖片</span></span>'"></span>`;
}
function scrollToPanel(panelId) {
const panel = document.getElementById(panelId);
if (!panel) return;
@@ -4155,7 +4253,7 @@ function renderGrowthSourceReadiness(sources) {
return;
}
box.innerHTML = sources.slice(0, 3).map((source) => {
box.innerHTML = sources.map((source) => {
const usable = Number(source.usable_offer_count || 0);
const detail = usable > 0
? `${source.data_quality_label || '資料可用'} · ${usable.toLocaleString()}`
@@ -5175,12 +5273,10 @@ function renderGrowthReviewCandidates(rows) {
const pchomePrice = row.pchome_price ? formatMoney(row.pchome_price) : '未取得 PChome 價格';
const pchomeUrl = safeHttpUrl(row.pchome_url);
const momoUrl = safeHttpUrl(row.momo_url || row.product_url);
const momoImageUrl = safeHttpUrl(row.image_url);
const pchomeLink = pchomeUrl ? `<a href="${escapeHtml(pchomeUrl)}" target="_blank" rel="noopener noreferrer">PChome 賣場</a>` : '<span class="text-muted">待補連結</span>';
const momoLink = momoUrl ? `<a href="${escapeHtml(momoUrl)}" target="_blank" rel="noopener noreferrer">MOMO 賣場</a>` : '<span class="text-muted">待補連結</span>';
const momoThumb = momoImageUrl
? `<img src="${escapeHtml(momoImageUrl)}" alt="MOMO 商品圖" loading="lazy">`
: '<i class="fas fa-store"></i>';
const pchomeThumb = renderProductThumb(row.pchome_image_url, `${row.pchome_product_name || 'PChome'} 商品圖`);
const momoThumb = renderProductThumb(row.momo_image_url || row.image_url, `${row.momo_title || 'MOMO'} 商品圖`);
const compareButton = pchomeUrl && momoUrl
? `<button type="button" class="btn btn-sm review-candidate-primary" data-pchome-url="${escapeHtml(pchomeUrl)}" data-momo-url="${escapeHtml(momoUrl)}" onclick="openReviewCandidateStores(this)"><i class="fas fa-up-right-from-square me-1"></i>同時開兩家賣場</button>`
: '';
@@ -5194,24 +5290,26 @@ function renderGrowthReviewCandidates(rows) {
<div class="review-candidate-compare" aria-label="兩家賣場比對">
<section class="review-candidate-store is-pchome">
<div class="review-candidate-store-head">
<span class="review-candidate-thumb"><i class="fas fa-store"></i></span>
${pchomeThumb}
<div>
<span class="review-candidate-store-label"><i class="fas fa-bolt"></i>PChome</span>
<span class="review-candidate-store-price">${escapeHtml(pchomePrice)}</span>
</div>
</div>
<p class="review-candidate-store-title" title="${escapeHtml(row.pchome_product_name || '')}">${escapeHtml(row.pchome_product_name || row.pchome_product_id || 'PChome 商品')}</p>
<div class="review-candidate-store-meta"><span>商品ID ${escapeHtml(row.pchome_product_id || '待補')}</span></div>
<div class="mt-2">${pchomeLink}</div>
</section>
<section class="review-candidate-store is-momo">
<div class="review-candidate-store-head">
<span class="review-candidate-thumb">${momoThumb}</span>
${momoThumb}
<div>
<span class="review-candidate-store-label"><i class="fas fa-store"></i>MOMO</span>
<span class="review-candidate-store-price">${escapeHtml(momoPrice)}</span>
</div>
</div>
<p class="review-candidate-store-title" title="${escapeHtml(row.momo_title || '')}">${escapeHtml(row.momo_title || row.momo_sku || '未命名候選')}</p>
<div class="review-candidate-store-meta"><span>商品ID ${escapeHtml(row.momo_sku || '待補')}</span></div>
<div class="mt-2">${momoLink}</div>
</section>
</div>
@@ -5612,9 +5710,9 @@ async function generatePickList() {
const btn = document.getElementById('btnPickList');
if (btn && btn.disabled) return;
btn.disabled = true;
if (btn) btn.disabled = true;
setActionBusy('generate-picks', true);
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
if (btn) btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/product-picks/generate', {
@@ -5634,9 +5732,9 @@ async function generatePickList() {
} catch (e) {
showToast('error', `整理失敗:${e.message}`, 4000);
} finally {
btn.disabled = false;
if (btn) btn.disabled = false;
setActionBusy('generate-picks', false);
btn.innerHTML = '<i class="fas fa-wand-magic-sparkles me-1"></i>產生今日清單';
if (btn) btn.innerHTML = '<i class="fas fa-list-check me-1"></i>產生今日清單';
}
}
@@ -5645,9 +5743,9 @@ async function backfillPchomeMatches() {
const btn = document.getElementById('btnBackfill');
if (btn && btn.disabled) return;
btn.disabled = true;
if (btn) btn.disabled = true;
setActionBusy('backfill', true);
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
if (btn) btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/pchome-match/backfill', {
@@ -5667,9 +5765,9 @@ async function backfillPchomeMatches() {
} catch (e) {
showToast('error', `商品對應失敗:${e.message}`, 4000);
} finally {
btn.disabled = false;
if (btn) btn.disabled = false;
setActionBusy('backfill', false);
btn.innerHTML = '<i class="fas fa-magnifying-glass-chart me-1"></i>補齊比價資料';
if (btn) btn.innerHTML = '<i class="fas fa-link me-1"></i>補齊比價資料';
}
}
@@ -5678,9 +5776,9 @@ async function triggerAnalysis() {
const btn = document.getElementById('btnTrigger');
if (btn && btn.disabled) return;
btn.disabled = true;
if (btn) btn.disabled = true;
setActionBusy('trigger-analysis', true);
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
if (btn) btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>整理中...';
try {
const res = await fetch('/api/ai/icaim/trigger', { method: 'POST' });
@@ -5699,9 +5797,9 @@ async function triggerAnalysis() {
} catch (e) {
showToast('error', `整理失敗:${e.message}`, 4000);
} finally {
btn.disabled = false;
if (btn) btn.disabled = false;
setActionBusy('trigger-analysis', false);
btn.innerHTML = '<i class="fas fa-bolt me-1"></i>更新今日建議';
if (btn) btn.innerHTML = '<i class="fas fa-sparkles me-1"></i>更新建議';
}
}
</script>

View File

@@ -622,7 +622,7 @@
</div>
{% endif %}
<div class="dashboard-table-wrap {% if current_filter == 'pchome_review' %}is-review-wrap{% endif %}">
<div class="dashboard-table-wrap {% if current_filter == 'pchome_review' %}is-review-wrap{% elif current_filter == 'ai_picks' %}is-ai-picks-wrap{% endif %}">
<table class="dashboard-table {% if current_filter == 'ai_picks' %}is-ai-picks{% elif current_filter == 'pchome_review' %}is-review{% endif %}">
<thead>
{% if current_filter == 'pchome_review' %}
@@ -676,7 +676,10 @@
<td>
<div class="dashboard-review-product-stack">
<div class="dashboard-product-cell">
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer">
<span class="dashboard-product-thumb-frame">
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="this.hidden=true; this.nextElementSibling.hidden=false;">
<span class="dashboard-product-thumb-missing" hidden aria-label="待補商品圖"><i class="fas fa-image" aria-hidden="true"></i></span>
</span>
<div>
<a class="dashboard-product-name momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
data-momo-original-url="{{ safe_product_url or '#' }}"
@@ -689,6 +692,9 @@
<span>MOMO {{ product.i_code }}</span>
<span>${{ item.record.price | int | number_format }}</span>
</div>
<div class="dashboard-product-identity" aria-label="商品身份">
<span>商品ID {{ product.i_code }}</span>
</div>
</div>
</div>
</div>
@@ -830,16 +836,22 @@
<td><span class="dashboard-category">{{ product.category or '未分類' }}</span></td>
<td>
<div class="dashboard-product-cell">
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer">
<span class="dashboard-product-thumb-frame">
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="this.hidden=true; this.nextElementSibling.hidden=false;">
<span class="dashboard-product-thumb-missing" hidden aria-label="待補商品圖"><i class="fas fa-image" aria-hidden="true"></i></span>
</span>
{% set safe_product_url = item.safe_momo_url or '#' %}
<a class="dashboard-product-name momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
data-momo-original-url="{{ safe_product_url or '#' }}"
data-track-platform="momo"
data-track-source="dashboard-v2-table-main"
data-track-product-id="{{ product.id }}"
data-track-icode="{{ product.i_code }}"
data-track-product-name="{{ product.name|e }}">{{ product.name }}</a>
<div>
<div class="dashboard-product-info">
<a class="dashboard-product-name momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
data-momo-original-url="{{ safe_product_url or '#' }}"
data-track-platform="momo"
data-track-source="dashboard-v2-table-main"
data-track-product-id="{{ product.id }}"
data-track-icode="{{ product.i_code }}"
data-track-product-name="{{ product.name|e }}">{{ product.name }}</a>
<div class="dashboard-product-identity" aria-label="商品身份">
<span>商品ID {{ product.i_code }}</span>
</div>
<div class="dashboard-platform-links">
<a class="dashboard-platform-link is-momo momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
data-momo-original-url="{{ safe_product_url or '#' }}"
@@ -950,6 +962,22 @@
{% endif %}
</div>
<div class="dashboard-ai-pick-reason">{{ item.ai_pick.reason }}</div>
<div class="dashboard-ai-action-row" aria-label="商品賣場連結">
<a class="dashboard-platform-link is-momo momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
data-momo-original-url="{{ safe_product_url or '#' }}"
data-track-platform="momo"
data-track-source="dashboard-v2-ai-pick-card"
data-track-product-id="{{ product.id }}"
data-track-icode="{{ product.i_code }}"
data-track-product-name="{{ product.name|e }}">
開 MOMO 賣場
</a>
{% if competitor and competitor.product_url %}
<a class="dashboard-platform-link is-pchome" href="{{ competitor.product_url }}" target="_blank" rel="noopener noreferrer">開 PChome 賣場</a>
{% elif item.pchome_match_attempt and item.pchome_match_attempt.competitor_product_url %}
<a class="dashboard-platform-link is-pchome" href="{{ item.pchome_match_attempt.competitor_product_url }}" target="_blank" rel="noopener noreferrer">開 PChome 候選</a>
{% endif %}
</div>
{% if item.ai_pick.missing_evidence %}
<div class="dashboard-ai-evidence-line" title="{{ item.ai_pick.missing_evidence_text }}">
{% for evidence in item.ai_pick.missing_evidence[:3] %}

View File

@@ -134,11 +134,11 @@
{% elif _page_group == 'ops' %}{% set _growth_stage = 'solve' %}
{% else %}{% set _growth_stage = 'govern' %}
{% endif %}
{% if _growth_stage == 'evaluate' %}{% set _growth_stage_brief = '現在:評估缺口' %}
{% elif _growth_stage == 'analyze' %}{% set _growth_stage_brief = '現在:分析原因' %}
{% elif _growth_stage == 'recommend' %}{% set _growth_stage_brief = '現在:產生建議' %}
{% elif _growth_stage == 'solve' %}{% set _growth_stage_brief = '現在:執行解法' %}
{% else %}{% set _growth_stage_brief = '現在:守住品質' %}
{% if _growth_stage == 'evaluate' %}{% set _growth_stage_brief = '今日重點:優先商品' %}
{% elif _growth_stage == 'analyze' %}{% set _growth_stage_brief = '今日重點:價格判斷' %}
{% elif _growth_stage == 'recommend' %}{% set _growth_stage_brief = '今日重點:業績建議' %}
{% elif _growth_stage == 'solve' %}{% set _growth_stage_brief = '今日重點:執行清單' %}
{% else %}{% set _growth_stage_brief = '今日重點:資料品質' %}
{% endif %}
<nav class="momo-growth-rail" aria-label="PChome 業績提升流程">
<span class="momo-growth-goal">