強化 MOMO PChome 比價可信鏈路
All checks were successful
CD Pipeline / deploy (push) Successful in 1m45s

This commit is contained in:
OoO
2026-05-19 15:26:10 +08:00
parent ec378258e7
commit dafde7e1a7
32 changed files with 1794 additions and 117 deletions

View File

@@ -813,6 +813,56 @@
margin-right: 0.4rem;
}
.daily-competitor-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-bottom: 14px;
}
.daily-competitor-summary > div {
min-width: 0;
padding: 10px;
border: 1px solid var(--momo-border-light);
border-radius: 6px;
background: var(--momo-bg-surface);
}
.daily-competitor-summary span,
.daily-competitor-risk-list span {
display: block;
color: var(--momo-text-secondary);
font-size: 0.72rem;
}
.daily-competitor-summary strong {
display: block;
margin-top: 4px;
color: var(--momo-text-primary);
font-size: clamp(1rem, 1.2vw, 1.35rem);
overflow-wrap: anywhere;
}
.daily-competitor-risk-list {
display: grid;
gap: 8px;
margin: 0;
padding-left: 1.1rem;
}
.daily-competitor-risk-list li {
padding: 8px 0;
border-bottom: 1px solid var(--momo-border-light);
}
.daily-competitor-risk-list li:last-child {
border-bottom: 0;
}
.daily-competitor-risk-list strong {
color: var(--momo-danger-text);
}
.card-header--split {
display: flex;
justify-content: space-between;

View File

@@ -353,6 +353,27 @@
display: grid;
}
.growth-analysis-page .ga-competitor-quality {
display: grid;
grid-template-columns: minmax(82px, 0.8fr) minmax(0, 1fr);
gap: 10px 12px;
align-items: center;
width: 100%;
}
.growth-analysis-page .ga-competitor-quality span {
color: var(--momo-text-secondary);
font-size: var(--momo-text-caption, 12px);
font-weight: 700;
}
.growth-analysis-page .ga-competitor-quality strong {
min-width: 0;
color: var(--momo-text-primary);
font-size: clamp(1rem, 1.5vw, 1.45rem);
overflow-wrap: anywhere;
}
@media (max-width: 640px) {
.growth-analysis-page .ga-page-head {
align-items: flex-start;

View File

@@ -76,6 +76,8 @@
// -- Safe data extraction ---------------------------------------------
const cd = dailySalesData.chartData || {};
const competitor = dailySalesData.competitor || {};
const competitorTrend = competitor.trend || {};
const safe = {
labels: cd.labels || [],
revenue: cd.revenue || [],
@@ -206,6 +208,12 @@
mode: 'currency',
horizontal: true
});
if (competitorTrend.labels) {
renderHtmlBars('competitorGapChart', competitorTrend.labels, competitorTrend.avg_gap_pct, {
mode: 'pct',
limit: 14
});
}
}
function hasSeriesData(labels, ...seriesList) {
@@ -429,6 +437,73 @@
}));
}
// -- Chart 5: Competitor gap pressure --------------------------------
function renderCompetitorGap() {
const el = document.getElementById('competitorGapChart');
if (!el) return;
const labels = competitorTrend.labels || [];
const avgGap = competitorTrend.avg_gap_pct || [];
const riskCount = competitorTrend.risk_count || [];
if (!hasSeriesData(labels, avgGap, riskCount)) {
renderChartEmpty('competitorGapChart', '目前沒有高信心 PChome 歷史價差序列。');
return;
}
rememberChart(new Chart(el, {
type: 'bar',
data: {
labels,
datasets: [
{
label: 'MOMO 較 PChome 貴 SKU 數',
data: riskCount,
backgroundColor: rgba(palette.rust, 0.28),
borderColor: palette.rust,
borderWidth: 1,
maxBarThickness: 24,
yAxisID: 'y'
},
{
label: '平均價差 %',
data: avgGap,
type: 'line',
borderColor: palette.caramel,
backgroundColor: rgba(palette.caramel, 0.12),
borderWidth: 2.2,
tension: 0.32,
pointRadius: 2,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: isCompact() ? 'bottom' : 'top' },
tooltip: {
callbacks: {
label: ctx => {
const mode = ctx.dataset.yAxisID === 'y1' ? 'pct' : 'number';
return `${ctx.dataset.label}: ${formatMetric(ctx.parsed.y, mode)}`;
}
}
}
},
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: isCompact() ? 5 : 10 } },
y: { beginAtZero: true, title: { display: !isCompact(), text: '風險 SKU 數' } },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
...axisPercent('平均價差')
}
}
}
}));
}
// -- Marketing charts ----------------------------------------------
function renderMarketingBar(elId, marketing, color) {
const el = document.getElementById(elId);
@@ -606,6 +681,7 @@
renderDod();
renderWow();
renderTop10();
renderCompetitorGap();
const mk = dailySalesData.marketing || {};
if (mk.discount) renderMarketingBar('discountChart', mk.discount, palette.caramel);

View File

@@ -129,6 +129,7 @@
renderHtmlBars('momChart', data.labels, data.mom, { mode: 'pct' });
renderHtmlBars('aovChart', data.labels, data.aov, { mode: 'currency' });
renderHtmlBars('marginChart', data.labels, data.margin_rate, { mode: 'pct' });
renderHtmlBars('competitorPressureChart', data.labels, data.competitor_gap_pct, { mode: 'pct' });
}
function hasSeriesData(labels, ...seriesList) {
@@ -169,6 +170,7 @@
const momEl = document.getElementById('momChart');
const aovEl = document.getElementById('aovChart');
const marginEl = document.getElementById('marginChart');
const competitorEl = document.getElementById('competitorPressureChart');
if (!revenueEl || !momEl || !aovEl || !marginEl) return;
if (!hasSeriesData(data.labels, data.revenue, data.yoy)) {
@@ -331,6 +333,70 @@
}
}));
}
if (competitorEl) {
if (!hasSeriesData(data.labels, data.competitor_gap_pct, data.competitor_risk_count)) {
renderChartEmpty('competitorPressureChart', '目前沒有可用的 PChome 月度價差序列。');
} else {
rememberChart(new Chart(competitorEl, {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: '價格壓力 SKU 數',
data: data.competitor_risk_count || [],
backgroundColor: chartPalette.rustSoft,
borderColor: chartPalette.rust,
borderWidth: 1,
maxBarThickness: 34,
yAxisID: 'y'
},
{
label: '平均價差 (%)',
data: data.competitor_gap_pct || [],
type: 'line',
borderColor: chartPalette.honey,
borderWidth: 2,
yAxisID: 'y1',
tension: 0.34,
cubicInterpolationMode: 'monotone',
pointRadius: 2,
pointHoverRadius: 5
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: isCompact() ? 'bottom' : 'top' },
tooltip: {
callbacks: {
label: ctx => {
const mode = ctx.dataset.yAxisID === 'y1' ? 'pct' : 'number';
return `${ctx.dataset.label}: ${formatMetric(ctx.parsed.y, mode)}`;
}
}
}
},
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: isCompact() ? 5 : 8 } },
y: {
beginAtZero: true,
title: { display: !isCompact(), text: '風險 SKU 數' }
},
y1: {
position: 'right',
grid: { drawOnChartArea: false },
...pctAxis('平均價差')
}
}
}
}));
}
}
stabilizeCharts();
}