This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user