V10.585 提升比價覆蓋工作台與每日業績圖表
All checks were successful
CD Pipeline / deploy (push) Successful in 1m17s

This commit is contained in:
OoO
2026-06-04 19:12:34 +08:00
parent a1e56a2c6b
commit 1dd4181fab
8 changed files with 671 additions and 69 deletions

View File

@@ -32,7 +32,7 @@
align-items: center;
gap: 10px;
font-family: var(--momo-font-family);
font-size: 26px;
font-size: 1.8rem;
font-weight: 800;
color: var(--momo-page-ink, var(--momo-text-primary));
line-height: var(--momo-line-height-tight);
@@ -576,6 +576,11 @@
background: color-mix(in srgb, var(--momo-text-primary) 5%, var(--momo-bg-surface));
border-radius: 4px;
}
.chart-container,
.chart-container--sm {
height: 280px;
}
}
@media (max-width: 767px) {
@@ -843,6 +848,27 @@
overflow-wrap: anywhere;
}
.daily-competitor-closure {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: -4px 0 12px;
}
.daily-competitor-closure span {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 3px 8px;
color: var(--momo-text-secondary);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-pill);
font-size: 0.72rem;
font-weight: 800;
white-space: nowrap;
}
.daily-competitor-risk-list {
display: grid;
gap: 8px;
@@ -944,6 +970,11 @@
height: 320px;
}
.chart-container--sm {
height: 210px;
margin-bottom: 14px;
}
.chart-container canvas {
display: block;
width: 100% !important;

View File

@@ -60,7 +60,7 @@
.dashboard-kpi-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
overflow: hidden;
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
@@ -120,7 +120,7 @@
.dashboard-kpi-value {
margin-bottom: 8px;
color: var(--momo-text-primary);
font-size: 34px;
font-size: 1.85rem;
font-weight: 800;
letter-spacing: 0;
line-height: 1;
@@ -153,6 +153,59 @@
font-size: 11px;
}
.dashboard-kpi-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin-top: 10px;
}
.dashboard-kpi-metrics span,
.dashboard-kpi-metrics a {
display: grid;
gap: 2px;
min-width: 0;
padding: 6px 8px;
color: var(--momo-text-secondary);
background: color-mix(in srgb, var(--momo-bg-paper) 86%, transparent);
border: 1px solid var(--momo-border-light);
border-radius: 6px;
text-decoration: none;
}
.dashboard-kpi-metrics em {
overflow: hidden;
color: var(--momo-text-tertiary);
font-size: 9px;
font-style: normal;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-kpi-metrics strong {
overflow: hidden;
color: var(--momo-text-primary);
font-size: 12px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-kpi.is-accent .dashboard-kpi-metrics span,
.dashboard-kpi.is-accent .dashboard-kpi-metrics a {
color: rgba(250, 247, 240, 0.76);
background: rgba(250, 247, 240, 0.08);
border-color: rgba(250, 247, 240, 0.16);
}
.dashboard-kpi.is-accent .dashboard-kpi-metrics em,
.dashboard-kpi.is-accent .dashboard-kpi-metrics strong {
color: rgba(250, 247, 240, 0.86);
}
.dashboard-kpi-sub-link {
color: inherit;
font-weight: 800;
@@ -216,6 +269,33 @@
overflow-wrap: anywhere;
}
.dashboard-backfill-pills {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 8px;
}
.dashboard-backfill-pills span {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 22px;
padding: 3px 7px;
color: var(--momo-text-secondary);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: var(--momo-radius-pill);
font-size: 10px;
font-weight: 800;
white-space: nowrap;
}
.dashboard-backfill-pills strong {
color: var(--momo-text-primary);
font-weight: 800;
}
.dashboard-backfill-progress {
position: relative;
width: 100%;
@@ -230,13 +310,13 @@
position: absolute;
inset: 0 auto 0 0;
width: 0%;
background: linear-gradient(90deg, var(--momo-warm-caramel), var(--momo-success));
background: var(--momo-warm-caramel);
transition: width 240ms ease;
}
.dashboard-backfill-card[data-status="failed"] .dashboard-backfill-progress span,
.dashboard-backfill-card[data-status="stale"] .dashboard-backfill-progress span {
background: linear-gradient(90deg, var(--momo-danger), var(--momo-warm-rust));
background: var(--momo-danger);
}
.dashboard-backfill-status {
@@ -252,6 +332,97 @@
min-width: 0;
}
.dashboard-decision-workbench {
display: grid;
grid-template-columns: minmax(220px, 0.8fr) minmax(0, 2.2fr);
gap: 12px;
margin-top: 12px;
padding: 14px 16px;
background: var(--momo-bg-surface);
border: 1px solid var(--momo-border-light);
border-radius: 8px;
}
.dashboard-decision-workbench__head {
display: grid;
gap: 4px;
align-content: start;
min-width: 0;
}
.dashboard-decision-workbench__head strong {
color: var(--momo-text-primary);
font-size: 15px;
font-weight: 800;
line-height: 1.25;
}
.dashboard-decision-workbench__head em {
color: var(--momo-text-secondary);
font-size: 11px;
font-style: normal;
line-height: 1.5;
}
.dashboard-decision-lanes {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
min-width: 0;
}
.dashboard-decision-lane {
display: grid;
grid-template-rows: auto auto 1fr;
gap: 5px;
min-width: 0;
min-height: 112px;
padding: 10px;
color: var(--momo-text-primary);
background: var(--momo-bg-paper);
border: 1px solid var(--momo-border-light);
border-radius: 8px;
text-align: left;
text-decoration: none;
transition: var(--momo-transition-base);
}
button.dashboard-decision-lane {
font: inherit;
cursor: pointer;
}
.dashboard-decision-lane:hover {
color: var(--momo-text-primary);
border-color: rgba(190, 106, 45, 0.38);
background: color-mix(in srgb, var(--momo-warm-caramel) 8%, var(--momo-bg-paper));
}
.dashboard-decision-lane span {
color: var(--momo-text-tertiary);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
}
.dashboard-decision-lane strong {
color: var(--momo-text-primary);
font-size: 13px;
font-weight: 800;
line-height: 1.25;
}
.dashboard-decision-lane em {
display: -webkit-box;
overflow: hidden;
color: var(--momo-text-secondary);
font-size: 11px;
font-style: normal;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.dashboard-focus-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1188,6 +1359,14 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.dashboard-decision-workbench {
grid-template-columns: 1fr;
}
.dashboard-decision-lanes {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dashboard-ai-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -1236,6 +1415,11 @@
}
.dashboard-kpi:nth-last-child(-n + 2) {
border-bottom: 1px solid var(--momo-border-light);
}
.dashboard-kpi:last-child {
grid-column: 1 / -1;
border-bottom: 0;
}
@@ -1254,6 +1438,21 @@
color: var(--momo-text-primary);
}
.dashboard-kpi.is-accent .dashboard-kpi-metrics span,
.dashboard-kpi.is-accent .dashboard-kpi-metrics a {
color: var(--momo-text-secondary);
background: var(--momo-bg-paper);
border-color: var(--momo-border-light);
}
.dashboard-kpi.is-accent .dashboard-kpi-metrics em {
color: var(--momo-text-tertiary);
}
.dashboard-kpi.is-accent .dashboard-kpi-metrics strong {
color: var(--momo-text-primary);
}
.dashboard-kpi-label {
margin-bottom: 7px;
font-size: 9px;
@@ -1288,6 +1487,14 @@
width: 100%;
}
.dashboard-decision-lanes {
grid-template-columns: 1fr;
}
.dashboard-decision-lane {
min-height: auto;
}
.dashboard-search,
.dashboard-select,
.dashboard-segmented {

View File

@@ -78,10 +78,17 @@
const cd = dailySalesData.chartData || {};
const competitor = dailySalesData.competitor || {};
const competitorTrend = competitor.trend || {};
const competitorCoverage = competitor.coverage || {};
const categoryChart = dailySalesData.categoryChart || {};
const safe = {
labels: cd.labels || [],
revenue: cd.revenue || [],
profit: cd.profit || [],
margin_rate: cd.margin_rate || (cd.revenue || []).map((revenue, index) => {
const rev = Number(revenue || 0);
if (!rev) return 0;
return Number((cd.profit || [])[index] || 0) / rev * 100;
}),
avg_price: cd.avg_price || [],
qty: cd.qty || [],
dod_revenue: cd.dod_revenue || [],
@@ -214,6 +221,19 @@
limit: 14
});
}
renderHtmlBars('marginChart', safe.labels, safe.margin_rate, { mode: 'pct', limit: 14 });
renderHtmlBars('avgQtyChart', safe.labels, safe.avg_price, { mode: 'currency', limit: 14 });
if (categoryChart.labels) {
renderHtmlBars('categoryRevenueChart', categoryChart.labels, categoryChart.revenue, {
mode: 'currency',
horizontal: true
});
}
const coverage = buildCoverageFunnel();
renderHtmlBars('competitorCoverageChart', coverage.labels, coverage.values, {
mode: 'number',
horizontal: true
});
}
function hasSeriesData(labels, ...seriesList) {
@@ -264,6 +284,20 @@
};
}
function buildCoverageFunnel() {
return {
labels: ['決策支援', '精準告警', '身份配對', '待刷新', '單位價', '待覆核'],
values: [
competitorCoverage.decision_support_count ?? competitorCoverage.decision_ready_count ?? 0,
competitorCoverage.decision_ready_matches ?? competitorCoverage.fresh_matches ?? 0,
competitorCoverage.valid_matches ?? 0,
competitorCoverage.stale_matches ?? competitorCoverage.stale_match_count ?? 0,
competitorCoverage.unit_comparable_count ?? 0,
competitorCoverage.rescore_accepted_count ?? competitorCoverage.review_queue_count ?? 0
].map(value => Number(value || 0))
};
}
// -- Chart 1: trend (multi-line) --------------------------------------
function renderTrend() {
const el = document.getElementById('trendChart');
@@ -440,7 +474,175 @@
}));
}
// -- Chart 5: Competitor gap pressure --------------------------------
// -- Chart 5: Margin rate trend --------------------------------------
function renderMarginRate() {
const el = document.getElementById('marginChart');
if (!el) return;
if (!hasSeriesData(safe.labels, safe.margin_rate)) {
renderChartEmpty('marginChart', '目前沒有可計算的毛利率序列。');
return;
}
rememberChart(new Chart(el, {
type: 'line',
data: {
labels: safe.labels,
datasets: [
makeLineDataset('毛利率', safe.margin_rate, palette.olive),
{
label: '30 日均線',
data: safe.margin_rate.map((_, index, values) => {
const start = Math.max(0, index - 6);
const sample = values.slice(start, index + 1).map(Number).filter(Number.isFinite);
return sample.length ? sample.reduce((sum, value) => sum + value, 0) / sample.length : 0;
}),
borderColor: palette.honey,
backgroundColor: rgba(palette.honey, 0.1),
borderWidth: 1.8,
borderDash: [5, 5],
tension: 0.28,
pointRadius: 0
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: isCompact() ? 'bottom' : 'top' },
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${formatMetric(ctx.parsed.y, 'pct')}` } }
},
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: isCompact() ? 5 : 10 } },
y: axisPercent('毛利率')
}
}
}));
}
// -- Chart 6: AOV x Qty ----------------------------------------------
function renderAvgQty() {
const el = document.getElementById('avgQtyChart');
if (!el) return;
if (!hasSeriesData(safe.labels, safe.avg_price, safe.qty)) {
renderChartEmpty('avgQtyChart', '目前沒有客單價或銷量序列。');
return;
}
rememberChart(new Chart(el, {
type: 'bar',
data: {
labels: safe.labels,
datasets: [
{
label: '銷量',
data: safe.qty,
backgroundColor: rgba(palette.olive, 0.24),
borderColor: palette.olive,
borderWidth: 1,
maxBarThickness: 24,
yAxisID: 'y'
},
{
label: '客單價',
data: safe.avg_price,
type: 'line',
borderColor: palette.mahogany,
backgroundColor: rgba(palette.mahogany, 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' ? 'currency' : '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: '銷量' } },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
...axisMoney('客單價')
}
}
}
}));
}
// -- Chart 7: Category revenue ---------------------------------------
function renderCategoryRevenue() {
const el = document.getElementById('categoryRevenueChart');
if (!el) return;
if (!hasSeriesData(categoryChart.labels, categoryChart.revenue, categoryChart.profit)) {
renderChartEmpty('categoryRevenueChart', '目前沒有分類業績彙總可繪製。');
return;
}
rememberChart(new Chart(el, {
type: 'bar',
data: {
labels: categoryChart.labels || [],
datasets: [
{
label: '業績',
data: categoryChart.revenue || [],
backgroundColor: rgba(palette.caramel, 0.5),
borderColor: palette.caramel,
borderWidth: 1,
maxBarThickness: 22
},
{
label: '毛利',
data: categoryChart.profit || [],
backgroundColor: rgba(palette.olive, 0.42),
borderColor: palette.olive,
borderWidth: 1,
maxBarThickness: 22
}
]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: isCompact() ? 'bottom' : 'top' },
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${formatMetric(ctx.parsed.x, 'currency')}` } }
},
scales: {
x: axisMoney('金額'),
y: {
grid: { display: false },
ticks: {
autoSkip: false,
callback: function (value) {
const label = this.getLabelForValue(value);
return label.length > 14 ? `${label.slice(0, 14)}` : label;
}
}
}
}
}
}));
}
// -- Chart 8: Competitor gap pressure --------------------------------
function renderCompetitorGap() {
const el = document.getElementById('competitorGapChart');
if (!el) return;
@@ -507,6 +709,59 @@
}));
}
// -- Chart 9: Competitor decision coverage ---------------------------
function renderCompetitorCoverage() {
const el = document.getElementById('competitorCoverageChart');
if (!el) return;
const coverage = buildCoverageFunnel();
if (!hasSeriesData(coverage.labels, coverage.values)) {
renderChartEmpty('competitorCoverageChart', '目前尚未形成可繪製的比價覆蓋資料。');
return;
}
rememberChart(new Chart(el, {
type: 'bar',
data: {
labels: coverage.labels,
datasets: [{
label: 'SKU 數',
data: coverage.values,
backgroundColor: [
rgba(palette.caramel, 0.62),
rgba(palette.olive, 0.54),
rgba(palette.honey, 0.54),
rgba(palette.rust, 0.38),
rgba(palette.mahogany, 0.32),
rgba(palette.muted, 0.24)
],
borderColor: [
palette.caramel,
palette.olive,
palette.honey,
palette.rust,
palette.mahogany,
palette.muted
],
borderWidth: 1,
maxBarThickness: 18
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${formatMetric(ctx.parsed.x, 'number')}` } }
},
scales: {
x: { beginAtZero: true, ticks: { precision: 0 } },
y: { grid: { display: false } }
}
}
}));
}
// -- Marketing charts ----------------------------------------------
function renderMarketingBar(elId, marketing, color) {
const el = document.getElementById(elId);
@@ -684,7 +939,11 @@
renderDod();
renderWow();
renderTop10();
renderMarginRate();
renderAvgQty();
renderCategoryRevenue();
renderCompetitorGap();
renderCompetitorCoverage();
const mk = dailySalesData.marketing || {};
if (mk.discount) renderMarketingBar('discountChart', mk.discount, palette.caramel);