修復分析頁新版殼層與暖色圖表
All checks were successful
CD Pipeline / deploy (push) Successful in 59s

This commit is contained in:
OoO
2026-05-06 21:01:07 +08:00
parent 11ccda0e1c
commit 7bc81e966b
7 changed files with 297 additions and 261 deletions

View File

@@ -37,8 +37,8 @@
color: var(--momo-text-primary, #2a2520);
}
.analysis-report-tab.is-active {
border-color: var(--momo-page-accent-dark, #65411f);
background: linear-gradient(135deg, var(--momo-page-accent, #8a5a2b), var(--momo-page-accent-dark, #65411f));
border-color: var(--momo-page-accent-dark, #a95846);
background: linear-gradient(135deg, var(--momo-page-accent, #d96f52), var(--momo-page-accent-dark, #a95846));
color: var(--momo-page-inverse, #fff8ee);
}
.analysis-report-tab.is-external {

View File

@@ -14,8 +14,8 @@
--momo-legacy-paper: #f3eee2;
--momo-legacy-ink: #2a2520;
--momo-legacy-muted: #645c52;
--momo-legacy-accent: #c96442;
--momo-legacy-accent-dark: #8f4530;
--momo-legacy-accent: #d96f52;
--momo-legacy-accent-dark: #a95846;
--momo-legacy-accent-soft: rgba(201, 100, 66, 0.12);
--momo-legacy-line: rgba(42, 37, 32, 0.16);
--momo-legacy-inverse: #fff8ee;
@@ -101,7 +101,7 @@
.btn-primary:hover,
.btn-primary:focus {
color: var(--momo-legacy-inverse) !important;
background: linear-gradient(135deg, #b1543a, #7a3520) !important;
background: linear-gradient(135deg, #c65f45, #9f4f3e) !important;
border-color: #7a3520 !important;
}

View File

@@ -130,16 +130,8 @@
font-weight: 600;
}
.trend-up {
color: #2ecc71;
}
.trend-down {
color: #e74c3c;
}
.bg-purple {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--momo-warm-rust), var(--momo-warm-mahogany));
}
.chart-container {
@@ -174,7 +166,7 @@
padding: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 2rem;
border: 1px solid rgba(102, 126, 234, 0.1);
border: 1px solid var(--momo-border-strong);
}
.calendar-header {
@@ -196,12 +188,12 @@
}
.calendar-header h5:hover {
color: #667eea;
color: var(--momo-warm-caramel);
transform: translateX(-4px);
}
.calendar-header h5:hover i {
color: #667eea;
color: var(--momo-warm-caramel);
transform: rotate(360deg);
transition: transform 0.5s ease;
}
@@ -212,7 +204,7 @@
}
.calendar-nav button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-mahogany));
border: none;
border-radius: 10px;
padding: 0.6rem 1.2rem;
@@ -221,12 +213,12 @@
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 600;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 12px rgba(217, 111, 82, 0.26);
}
.calendar-nav button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
box-shadow: 0 6px 16px rgba(217, 111, 82, 0.32);
}
.calendar-nav button:active {
@@ -261,19 +253,19 @@
.calendar-day:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
border-color: #667eea;
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
box-shadow: 0 8px 20px rgba(217, 111, 82, 0.16);
border-color: var(--momo-warm-caramel);
background: linear-gradient(135deg, #ffffff 0%, var(--momo-warm-peach-soft) 100%);
}
.calendar-day.has-data {
background: linear-gradient(135deg, #ffffff 0%, #fafbff 100%);
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 248, 238, 0.9) 100%);
border-color: #d0d5e0;
}
.calendar-day.has-data:hover {
border-color: #667eea;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2);
border-color: var(--momo-warm-caramel);
box-shadow: 0 8px 24px rgba(217, 111, 82, 0.2);
}
.calendar-day.other-month {
@@ -282,10 +274,10 @@
}
.calendar-day.selected {
border-color: #667eea !important;
border-color: var(--momo-warm-caramel) !important;
border-width: 5px !important;
box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.25), 0 12px 32px rgba(102, 126, 234, 0.4), inset 0 2px 10px rgba(102, 126, 234, 0.15) !important;
background: linear-gradient(135deg, #e8edff 0%, #d0d9ff 100%) !important;
box-shadow: 0 0 0 6px rgba(217, 111, 82, 0.22), 0 12px 32px rgba(217, 111, 82, 0.28), inset 0 2px 10px rgba(217, 111, 82, 0.12) !important;
background: linear-gradient(135deg, var(--momo-warm-peach-soft) 0%, var(--momo-warm-caramel-soft) 100%) !important;
transform: scale(1.08) !important;
position: relative;
animation: pulseGlow 2s ease-in-out infinite;
@@ -298,7 +290,7 @@
right: -8px;
width: 32px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-mahogany));
border-radius: 50%;
color: white;
display: flex;
@@ -306,7 +298,7 @@
justify-content: center;
font-size: 1.2rem;
font-weight: 900;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
box-shadow: 0 4px 12px rgba(217, 111, 82, 0.36);
z-index: 10;
}
@@ -319,11 +311,11 @@
0%,
100% {
box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.25), 0 12px 32px rgba(102, 126, 234, 0.4), inset 0 2px 10px rgba(102, 126, 234, 0.15);
box-shadow: 0 0 0 6px rgba(217, 111, 82, 0.22), 0 12px 32px rgba(217, 111, 82, 0.28), inset 0 2px 10px rgba(217, 111, 82, 0.12);
}
50% {
box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.35), 0 16px 40px rgba(102, 126, 234, 0.5), inset 0 2px 10px rgba(102, 126, 234, 0.2);
box-shadow: 0 0 0 8px rgba(217, 111, 82, 0.28), 0 16px 40px rgba(217, 111, 82, 0.34), inset 0 2px 10px rgba(217, 111, 82, 0.16);
}
}
@@ -424,17 +416,17 @@
.date-selector:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
border-color: var(--momo-warm-caramel);
box-shadow: 0 0 0 3px rgba(217, 111, 82, 0.12);
}
/* 頁面標題樣式 */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-mahogany));
padding: 2rem;
border-radius: 16px;
margin-bottom: 2rem;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.25);
box-shadow: 0 8px 24px rgba(217, 111, 82, 0.22);
}
.page-header h4 {
@@ -462,19 +454,19 @@
/* 按鈕樣式優化 */
.btn-success {
background: linear-gradient(135deg, #51cf66 0%, #37b24d 100%);
background: linear-gradient(135deg, var(--momo-warm-earth), var(--momo-warm-honey));
border: none;
border-radius: 10px;
padding: 0.6rem 1.5rem;
font-weight: 600;
box-shadow: 0 4px 12px rgba(81, 207, 102, 0.3);
box-shadow: 0 4px 12px rgba(154, 143, 89, 0.24);
transition: all 0.3s ease;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(81, 207, 102, 0.4);
background: linear-gradient(135deg, #40c057 0%, #2f9e44 100%);
box-shadow: 0 6px 16px rgba(154, 143, 89, 0.32);
background: linear-gradient(135deg, var(--momo-warm-honey), var(--momo-warm-earth));
}
/* 表格樣式優化 */
@@ -484,7 +476,7 @@
}
.table thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--momo-warm-mahogany), var(--momo-warm-caramel));
color: #fff;
}
@@ -1307,10 +1299,33 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
<script>
{% if not error %}
if (typeof Chart === 'undefined') {
console.error('Chart.js 未載入,無法繪製當日業績圖表');
}
Chart.defaults.color = '#6f665a';
Chart.defaults.borderColor = 'rgba(126, 111, 92, 0.18)';
Chart.defaults.font.family = "'Noto Sans TC', 'Inter', system-ui, sans-serif";
const chartPalette = {
caramel: 'rgba(201, 100, 66, 1)',
caramelSoft: 'rgba(201, 100, 66, 0.14)',
honey: 'rgba(184, 132, 22, 1)',
honeySoft: 'rgba(184, 132, 22, 0.14)',
rust: 'rgba(181, 52, 47, 1)',
rustSoft: 'rgba(181, 52, 47, 0.12)',
mahogany: 'rgba(143, 69, 48, 1)',
mahoganySoft: 'rgba(143, 69, 48, 0.12)',
earth: 'rgba(138, 90, 43, 1)',
earthSoft: 'rgba(138, 90, 43, 0.12)',
muted: 'rgba(126, 111, 92, 0.52)'
};
const chartData = {{ chart_data | tojson }};
// 調試:檢查數據
@@ -1345,8 +1360,8 @@
{
label: '業績',
data: safeData.revenue,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: chartPalette.caramel,
backgroundColor: chartPalette.caramelSoft,
borderWidth: 2,
yAxisID: 'y',
tension: 0.3,
@@ -1355,8 +1370,8 @@
{
label: '毛利',
data: safeData.profit,
borderColor: 'rgba(46, 204, 113, 1)',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderColor: chartPalette.honey,
backgroundColor: chartPalette.honeySoft,
borderWidth: 2,
yAxisID: 'y',
tension: 0.3,
@@ -1365,8 +1380,8 @@
{
label: '客單價',
data: safeData.avg_price,
borderColor: 'rgba(153, 102, 255, 1)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderColor: chartPalette.mahogany,
backgroundColor: chartPalette.mahoganySoft,
borderWidth: 2,
yAxisID: 'y1',
tension: 0.3,
@@ -1375,8 +1390,8 @@
{
label: '銷量',
data: safeData.qty,
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
borderColor: chartPalette.earth,
backgroundColor: chartPalette.earthSoft,
borderWidth: 2,
yAxisID: 'y2',
tension: 0.3,
@@ -1403,7 +1418,7 @@
display: true,
position: 'left',
beginAtZero: true,
title: { display: true, text: '業績/毛利 ($)', color: '#54a0ff' }
title: { display: true, text: '業績/毛利 ($)', color: chartPalette.caramel }
},
y1: {
type: 'linear',
@@ -1411,7 +1426,7 @@
position: 'right',
beginAtZero: true,
grid: { drawOnChartArea: false },
title: { display: true, text: '客單價 ($)', color: '#9966ff' }
title: { display: true, text: '客單價 ($)', color: chartPalette.mahogany }
},
y2: {
type: 'linear',
@@ -1433,8 +1448,8 @@
{
label: '業績 DoD%',
data: safeData.dod_revenue,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: chartPalette.caramel,
backgroundColor: chartPalette.caramelSoft,
borderWidth: 2,
tension: 0.3,
fill: false
@@ -1442,8 +1457,8 @@
{
label: '毛利 DoD%',
data: safeData.dod_profit,
borderColor: 'rgba(46, 204, 113, 1)',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderColor: chartPalette.honey,
backgroundColor: chartPalette.honeySoft,
borderWidth: 2,
tension: 0.3,
fill: false
@@ -1451,8 +1466,8 @@
{
label: '客單 DoD%',
data: safeData.dod_avg_price,
borderColor: 'rgba(153, 102, 255, 1)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderColor: chartPalette.mahogany,
backgroundColor: chartPalette.mahoganySoft,
borderWidth: 2,
tension: 0.3,
fill: false
@@ -1460,8 +1475,8 @@
{
label: '銷量 DoD%',
data: safeData.dod_qty,
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
borderColor: chartPalette.earth,
backgroundColor: chartPalette.earthSoft,
borderWidth: 2,
tension: 0.3,
fill: false
@@ -1506,57 +1521,57 @@
{
label: '業績 WoW%',
data: safeData.wow_revenue,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: chartPalette.caramel,
backgroundColor: chartPalette.caramelSoft,
borderWidth: 2,
tension: 0.3,
fill: false,
segment: {
borderColor: ctx => {
// 前 7 天顯示為淺灰色
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(54, 162, 235, 1)';
return ctx.p0DataIndex < 7 ? chartPalette.muted : chartPalette.caramel;
}
}
},
{
label: '毛利 WoW%',
data: safeData.wow_profit,
borderColor: 'rgba(46, 204, 113, 1)',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderColor: chartPalette.honey,
backgroundColor: chartPalette.honeySoft,
borderWidth: 2,
tension: 0.3,
fill: false,
segment: {
borderColor: ctx => {
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(46, 204, 113, 1)';
return ctx.p0DataIndex < 7 ? chartPalette.muted : chartPalette.honey;
}
}
},
{
label: '客單 WoW%',
data: safeData.wow_avg_price,
borderColor: 'rgba(153, 102, 255, 1)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderColor: chartPalette.mahogany,
backgroundColor: chartPalette.mahoganySoft,
borderWidth: 2,
tension: 0.3,
fill: false,
segment: {
borderColor: ctx => {
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(153, 102, 255, 1)';
return ctx.p0DataIndex < 7 ? chartPalette.muted : chartPalette.mahogany;
}
}
},
{
label: '銷量 WoW%',
data: safeData.wow_qty,
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
borderColor: chartPalette.earth,
backgroundColor: chartPalette.earthSoft,
borderWidth: 2,
tension: 0.3,
fill: false,
segment: {
borderColor: ctx => {
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(255, 159, 64, 1)';
return ctx.p0DataIndex < 7 ? chartPalette.muted : chartPalette.earth;
}
}
}
@@ -1610,8 +1625,8 @@
datasets: [{
label: '銷售金額',
data: safeData.top10_values,
backgroundColor: 'rgba(255, 159, 64, 0.6)',
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(201, 100, 66, 0.62)',
borderColor: chartPalette.caramel,
borderWidth: 1
}]
},

View File

@@ -38,9 +38,9 @@
.momo-app[data-active-page="daily_sales"],
.momo-app[data-active-page="monthly"],
.momo-app[data-active-page="growth"] {
--momo-page-accent: var(--momo-warm-earth);
--momo-page-accent-dark: #65411f;
--momo-page-accent-soft: var(--momo-warm-earth-soft);
--momo-page-accent: var(--momo-warm-caramel);
--momo-page-accent-dark: var(--momo-warm-mahogany);
--momo-page-accent-soft: var(--momo-warm-peach-soft);
}
.momo-app[data-active-page="vendor_stockout"] {
--momo-page-accent: var(--momo-warm-rust);
@@ -57,7 +57,7 @@
.momo-app[data-active-page="auto_import"],
.momo-app[data-active-page="market_intel"] {
--momo-page-accent: var(--momo-warm-mahogany);
--momo-page-accent-dark: #5e2e20;
--momo-page-accent-dark: #7f3f32;
--momo-page-accent-soft: var(--momo-warm-mahogany-soft);
}
.momo-app[data-active-page^="obs_"],

View File

@@ -1,33 +1,62 @@
<!-- cspell:ignore MOMO -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>營運成長報表 - MOMO 監控系統</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
{% extends 'ewoooc_base.html' %}
{% block title %}營運成長報表 - EwoooC{% endblock %}
{% block extra_css %}
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f6f9; }
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.card { border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.03); margin-bottom: 1.5rem; transition: all 0.3s ease; background: #fff; }
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
.card-header { background-color: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); font-weight: 700; color: #2c3e50; padding: 1.25rem; }
.growth-analysis-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.growth-analysis-page .card {
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
box-shadow: var(--momo-shadow-soft);
margin-bottom: 1.5rem;
background: rgba(255, 253, 248, 0.94);
}
.growth-analysis-page .card-header {
background: rgba(250, 247, 240, 0.9);
border-bottom: 1px solid var(--momo-border-subtle);
font-weight: 800;
color: var(--momo-text-strong);
padding: 1rem 1.25rem;
}
.kpi-card { position: relative; overflow: hidden; border: none; }
.kpi-card .icon-bg { position: absolute; right: -15px; bottom: -15px; font-size: 6rem; opacity: 0.15; transform: rotate(-15deg); pointer-events: none; }
.kpi-value { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 0.2rem; }
.kpi-label { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; }
.trend-up { color: #2ecc71; }
.trend-down { color: #e74c3c; }
</style>
</head>
<body class="bg-body-tertiary">
{% include 'components/_navbar.html' %}
.growth-analysis-page .bg-primary,
.growth-analysis-page .bg-success,
.growth-analysis-page .bg-info {
background: linear-gradient(135deg, var(--momo-page-accent-dark), var(--momo-page-accent)) !important;
}
<div class="container-fluid px-4">
.growth-analysis-page .bg-success {
background: linear-gradient(135deg, var(--momo-warm-earth), var(--momo-warm-honey)) !important;
}
.growth-analysis-page .bg-info {
background: linear-gradient(135deg, var(--momo-warm-rust), var(--momo-warm-caramel)) !important;
}
.growth-analysis-page .text-success,
.growth-analysis-page .trend-up {
color: var(--momo-warm-honey) !important;
}
.growth-analysis-page .trend-down {
color: var(--momo-warm-rust) !important;
}
</style>
{% endblock %}
{% block ewooo_content %}
<div class="growth-analysis-page">
{% include 'components/_analysis_report_tabs.html' %}
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
<h4 class="mb-0 fw-bold text-dark"><i class="fas fa-rocket me-2 text-success"></i>營運成長策略報表</h4>
@@ -135,9 +164,11 @@
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Data Injection -->
<script id="chart-data" type="application/json">
{{ chart_data | tojson }}
@@ -145,6 +176,20 @@
<script>
const data = JSON.parse(document.getElementById('chart-data').textContent);
const chartPalette = {
caramel: 'rgba(201, 100, 66, 1)',
caramelSoft: 'rgba(201, 100, 66, 0.58)',
honey: 'rgba(184, 132, 22, 1)',
honeySoft: 'rgba(184, 132, 22, 0.58)',
rust: 'rgba(181, 52, 47, 1)',
rustSoft: 'rgba(181, 52, 47, 0.48)',
mahogany: 'rgba(143, 69, 48, 1)',
mahoganySoft: 'rgba(143, 69, 48, 0.12)'
};
Chart.defaults.color = '#6f665a';
Chart.defaults.borderColor = 'rgba(126, 111, 92, 0.18)';
Chart.defaults.font.family = "'Noto Sans TC', 'Inter', system-ui, sans-serif";
// 1. Revenue & YoY Chart (Mixed)
new Chart(document.getElementById('revenueChart'), {
@@ -155,14 +200,14 @@
{
label: '月營收 ($)',
data: data.revenue,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
backgroundColor: chartPalette.caramelSoft,
order: 2
},
{
label: 'YoY 年增率 (%)',
data: data.yoy,
type: 'line',
borderColor: '#ff6384',
borderColor: chartPalette.rust,
borderWidth: 2,
yAxisID: 'y1',
order: 1,
@@ -194,7 +239,7 @@
data: data.mom,
backgroundColor: (ctx) => {
const val = ctx.raw;
return val >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)';
return val >= 0 ? chartPalette.honeySoft : chartPalette.rustSoft;
}
}]
},
@@ -213,8 +258,8 @@
datasets: [{
label: '平均客單價 ($)',
data: data.aov,
borderColor: '#36a2eb',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: chartPalette.caramel,
backgroundColor: 'rgba(201, 100, 66, 0.12)',
fill: true,
tension: 0.4
}]
@@ -234,8 +279,8 @@
datasets: [{
label: '毛利率 (%)',
data: data.margin_rate,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderColor: chartPalette.honey,
backgroundColor: 'rgba(184, 132, 22, 0.12)',
fill: true,
tension: 0.4
}]
@@ -247,5 +292,4 @@
}
});
</script>
</body>
</html>
{% endblock %}

View File

@@ -1,78 +1,15 @@
<!-- cspell:ignore MOMO datatables Treemap -->
<!DOCTYPE html>
<html lang="zh-TW">
{% extends 'ewoooc_base.html' %}
{% block title %}業績分析 - EwoooC{% endblock %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>業績分析 - EwoooC</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% block extra_css %}
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
<!-- V-New: Flatpickr 日期選擇器 CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
<!-- V-Fix: 使用 Chart.js v3.9.1 以確保與 Treemap v2.0.2 相容 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@2.0.2/dist/chartjs-chart-treemap.min.js"></script>
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f6f9;
padding-top: 70px;
}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9) !important;
font-weight: 500;
transition: all 0.3s;
}
.navbar-dark .navbar-nav .nav-link:hover {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.navbar-dark .navbar-nav .nav-link.active {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.15);
border-radius: 6px;
font-weight: 600;
}
.navbar-dark .navbar-text {
color: rgba(255, 255, 255, 0.8) !important;
}
.navbar {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
}
/* V-Fix: 防止導航列 dropdown 展開時背景變黑 */
.navbar-dark .nav-link.active,
.navbar-dark .nav-link.show,
.navbar-dark .nav-link:focus {
background-color: transparent !important;
}
.navbar-dark .dropdown-toggle.active::after,
.navbar-dark .dropdown-toggle.show::after {
color: #fff !important;
background-color: var(--momo-bg-canvas);
}
/* V-Opt: 現代化卡片風格 */
@@ -217,12 +154,12 @@
font-size: 2.5rem;
font-weight: 800;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%);
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-rust));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 4px;
filter: drop-shadow(0 4px 15px rgba(79, 70, 229, 0.4));
filter: drop-shadow(0 4px 15px rgba(217, 111, 82, 0.32));
}
@@ -273,8 +210,8 @@
width: 150px;
height: 150px;
border: 4px solid transparent;
border-top-color: #4F46E5;
border-right-color: #7C3AED;
border-top-color: var(--momo-warm-caramel);
border-right-color: var(--momo-warm-rust);
border-radius: 50%;
animation: ring-spin 2s linear infinite;
}
@@ -345,9 +282,9 @@
position: absolute;
width: 8px;
height: 8px;
background: linear-gradient(135deg, #4F46E5, #7C3AED);
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-rust));
border-radius: 50%;
box-shadow: 0 0 10px rgba(79, 70, 229, 0.8);
box-shadow: 0 0 10px rgba(217, 111, 82, 0.68);
}
#loadingOverlay .orbit-particle:nth-child(1) {
@@ -452,7 +389,7 @@
#loadingOverlay .loading-text {
font-size: 1.2rem;
color: #4F46E5;
color: var(--momo-warm-mahogany);
font-weight: 600;
text-align: center;
letter-spacing: 0.5px;
@@ -468,14 +405,14 @@
#loadingOverlay .loading-progress {
width: 200px;
height: 4px;
background: rgba(79, 70, 229, 0.2);
background: rgba(217, 111, 82, 0.18);
border-radius: 2px;
overflow: hidden;
}
#loadingOverlay .loading-progress-bar {
height: 100%;
background: linear-gradient(90deg, #4F46E5, #7C3AED, #4F46E5);
background: linear-gradient(90deg, var(--momo-warm-caramel), var(--momo-warm-rust), var(--momo-warm-honey));
background-size: 200% 100%;
animation: progress-flow 1.5s linear infinite;
width: 100%;
@@ -562,11 +499,36 @@
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
</style>
</head>
.sales-analysis-page .card,
.sales-analysis-page .panel,
.sales-analysis-page .filter-section {
border-radius: 8px;
}
<body class="bg-body-tertiary">
{% include 'components/_navbar.html' %}
.sales-analysis-page .bg-primary,
.sales-analysis-page .btn-primary {
background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-mahogany)) !important;
border-color: var(--momo-warm-mahogany) !important;
}
.sales-analysis-page .btn-success,
.sales-analysis-page .bg-success {
background: linear-gradient(135deg, var(--momo-warm-earth), var(--momo-warm-honey)) !important;
border-color: var(--momo-warm-earth) !important;
}
.sales-analysis-page .text-primary,
.sales-analysis-page .text-success {
color: var(--momo-warm-caramel) !important;
}
.sales-analysis-page .badge.bg-secondary {
background-color: rgba(111, 102, 90, 0.86) !important;
}
</style>
{% endblock %}
{% block ewooo_content %}
<!-- Loading Overlay -->
<div id="loadingOverlay">
@@ -607,7 +569,7 @@
</div>
</div>
<div class="container-fluid px-4">
<div class="sales-analysis-page">
{% include 'components/_analysis_report_tabs.html' %}
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
<div class="d-flex align-items-center gap-3">
@@ -651,29 +613,26 @@
<!-- V-New: 控制面板 (篩選器) -->
<div class="card mb-4 shadow-sm" style="z-index: 100; transform: none !important; transition: none;">
<div class="card-header bg-gradient"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none;">
style="background: linear-gradient(135deg, var(--momo-warm-caramel), var(--momo-warm-mahogany)); border: none;">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-white"><i class="fas fa-sliders-h me-2"></i>進階篩選與分析</h5>
<!-- V-New: 分析維度切換(更顯眼的設計) -->
<div class="btn-group shadow-sm" role="group">
<input type="radio" class="btn-check" name="metric" id="metricAmount" value="amount"
autocomplete="off" onchange="setFilter('metric', 'amount')" {% if selected_metric=='amount'
%}checked{% endif %}>
autocomplete="off" onchange="setFilter('metric', 'amount')" {% if selected_metric=='amount' %}checked{% endif %}>
<label class="btn btn-sm btn-light fw-bold" for="metricAmount">
<i class="fas fa-dollar-sign me-1"></i>依金額分析
</label>
<input type="radio" class="btn-check" name="metric" id="metricQty" value="qty"
autocomplete="off" onchange="setFilter('metric', 'qty')" {% if selected_metric=='qty'
%}checked{% endif %}>
autocomplete="off" onchange="setFilter('metric', 'qty')" {% if selected_metric=='qty' %}checked{% endif %}>
<label class="btn btn-sm btn-light fw-bold" for="metricQty">
<i class="fas fa-box me-1"></i>依銷售量分析
</label>
{% if cols.cost or cols.profit %}
<input type="radio" class="btn-check" name="metric" id="metricProfit" value="profit"
autocomplete="off" onchange="setFilter('metric', 'profit')" {% if selected_metric=='profit'
%}checked{% endif %}>
autocomplete="off" onchange="setFilter('metric', 'profit')" {% if selected_metric=='profit' %}checked{% endif %}>
<label class="btn btn-sm btn-light fw-bold" for="metricProfit">
<i class="fas fa-chart-line me-1"></i>依毛利分析
</label>
@@ -701,16 +660,11 @@
onchange="handleDataRangeChange(this)">
<option value="" {% if not request.args.get('data_range') %}selected{% endif %}>
-- 請選擇 --</option>
<option value="1" {% if request.args.get('data_range')=='1' %}selected{% endif
%}>最近 1 個月 (推薦)</option>
<option value="3" {% if request.args.get('data_range')=='3' %}selected{% endif
%}>最近 3 個月</option>
<option value="6" {% if request.args.get('data_range')=='6' %}selected{% endif
%}>最近 6 個月</option>
<option value="12" {% if request.args.get('data_range')=='12' %}selected{% endif
%}>最近 12 個月</option>
<option value="0" {% if request.args.get('data_range')=='0' %}selected{% endif
%}>全部資料</option>
<option value="1" {% if request.args.get('data_range')=='1' %}selected{% endif %}>最近 1 個月 (推薦)</option>
<option value="3" {% if request.args.get('data_range')=='3' %}selected{% endif %}>最近 3 個月</option>
<option value="6" {% if request.args.get('data_range')=='6' %}selected{% endif %}>最近 6 個月</option>
<option value="12" {% if request.args.get('data_range')=='12' %}selected{% endif %}>最近 12 個月</option>
<option value="0" {% if request.args.get('data_range')=='0' %}selected{% endif %}>全部資料</option>
</select>
</div>
@@ -1750,14 +1704,18 @@
</div>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<!-- Chart.js v3 + Treemap v2 are paired intentionally. -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@2.0.2/dist/chartjs-chart-treemap.min.js"></script>
<!-- DataTables JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
{% endif %}
{% if not error %}
<!-- V-New: JSON Data Block for Data Injection -->
<script id="sales-data" type="application/json">
@@ -1960,6 +1918,20 @@
const treemapDataPoints = salesData.treemapData;
const cols = salesData.cols;
const selectedMetric = salesData.selectedMetric;
const momoChartColors = {
coral: 'rgba(217, 111, 82, 0.64)',
coralLine: 'rgba(217, 111, 82, 1)',
peach: 'rgba(242, 159, 126, 0.62)',
amber: 'rgba(214, 161, 47, 0.64)',
amberLine: 'rgba(214, 161, 47, 1)',
rose: 'rgba(200, 95, 106, 0.62)',
roseLine: 'rgba(200, 95, 106, 1)',
clay: 'rgba(169, 88, 70, 0.62)',
clayLine: 'rgba(169, 88, 70, 1)',
olive: 'rgba(154, 143, 89, 0.62)',
oliveLine: 'rgba(154, 143, 89, 1)',
neutral: 'rgba(111, 102, 90, 0.42)'
};
// 1. 橫向長條圖 (Horizontal Bar Chart) - 更易讀
const ctxBar = document.getElementById('barChart').getContext('2d');
@@ -1970,8 +1942,8 @@
datasets: [{
label: barData.metricLabel,
data: barData.values,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: momoChartColors.coral,
borderColor: momoChartColors.coralLine,
borderWidth: 1
}]
},
@@ -2003,7 +1975,14 @@
labels: catData.labels,
datasets: [{
data: catData.values,
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#C9CBCF']
backgroundColor: [
momoChartColors.coral,
momoChartColors.peach,
momoChartColors.amber,
momoChartColors.rose,
momoChartColors.olive,
momoChartColors.neutral
]
}]
},
options: {
@@ -2044,8 +2023,8 @@
datasets: [{
label: '區間總業績 ($)',
data: priceDistData.values,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: momoChartColors.amber,
borderColor: momoChartColors.amberLine,
borderWidth: 1
}]
},
@@ -2127,8 +2106,8 @@
datasets: [{
label: metricLabel,
data: mkt.discount.map(i => i[metricKey] || 0),
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: momoChartColors.peach,
borderColor: momoChartColors.coralLine,
borderWidth: 1
}]
},
@@ -2162,8 +2141,8 @@
datasets: [{
label: metricLabel,
data: mkt.coupon.map(i => i[metricKey] || 0),
backgroundColor: 'rgba(75, 192, 192, 0.7)',
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: momoChartColors.amber,
borderColor: momoChartColors.amberLine,
borderWidth: 1
}]
},
@@ -2385,8 +2364,8 @@
datasets: [{
label: '月總業績 ($)',
data: monthlyData.values,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: momoChartColors.coral,
borderColor: momoChartColors.coralLine,
borderWidth: 1
}]
},
@@ -2414,8 +2393,8 @@
datasets: [{
label: '週總業績 ($)',
data: weeklyData.values,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(214, 161, 47, 0.16)',
borderColor: momoChartColors.amberLine,
borderWidth: 2,
fill: true,
tension: 0.3
@@ -2453,8 +2432,8 @@
datasets: [{
label: '時段總業績 ($)',
data: hourlyData.values,
backgroundColor: 'rgba(153, 102, 255, 0.2)',
borderColor: 'rgba(153, 102, 255, 1)',
backgroundColor: 'rgba(200, 95, 106, 0.14)',
borderColor: momoChartColors.roseLine,
borderWidth: 2,
fill: true,
tension: 0.3 // 平滑曲線
@@ -3160,8 +3139,4 @@
window.endDatePicker = endPicker;
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{% endblock %}

View File

@@ -1,7 +1,7 @@
/**
* MOMO Pro × Nothing × Claude 設計 Token v2.0
* — Nothing 的點陣骨架(黑白、像素、工業)
* — Claude 的暖米基底(#f0eee9、焦糖橘 #c96442
* — Claude 的暖米基底(#f0eee9、EwoooC 珊瑚橘 #d96f52
*/
:root {
@@ -22,32 +22,34 @@
--momo-line-soft: rgba(42,37,32,0.18);
--momo-line-faint: rgba(42,37,32,0.10);
/* Claude 焦糖accent */
--momo-accent: #c96442; /* 主 accent */
--momo-accent-50: #fbf2ef;
--momo-accent-100: #f5e1d9;
--momo-accent-200: #ecc3b3;
--momo-accent-500: #c96442;
--momo-accent-600: #b1543a;
--momo-accent-700: #8f4530;
--momo-accent-soft: rgba(201,100,66,0.12);
/* EwoooC 珊瑚accent */
--momo-accent: #d96f52; /* 主 accent */
--momo-accent-50: #fff4ef;
--momo-accent-100: #fde4d8;
--momo-accent-200: #f8c6b3;
--momo-accent-500: #d96f52;
--momo-accent-600: #c65f45;
--momo-accent-700: #9f4f3e;
--momo-accent-soft: rgba(217,111,82,0.13);
--momo-accent-strong: var(--momo-accent-700);
/* ===== EwoooC 暖色家族(全站運用) =====
* 全部留在暖色域(紅/橘/金/土),不混入冷色
* 全部留在暖色域(珊瑚/蜜桃/琥珀/玫瑰銅/暖橄欖),避免髒棕與冷藍紫
* 用法:活動頁 / 標籤色 / 圖表分類色 / 各區段視覺主軸
*/
--momo-warm-caramel: #c96442; /* 焦糖橘 — 主 accent / 限時搶購 */
--momo-warm-honey: #b88416; /* 蜂蜜金 — 1.1 狂歡 / 警示 */
--momo-warm-rust: #b5342f; /* 暖紅 — 母親節 / danger */
--momo-warm-mahogany: #8f4530; /* 深焦糖 — 520 情人節 / 強調 */
--momo-warm-earth: #8a5a2b; /* 焦土 — 勞動節 / 中性暖 */
--momo-warm-caramel: #d96f52; /* 珊瑚橘 — 主 accent / 主要動作 */
--momo-warm-peach: #f29f7e; /* 蜜桃 — hover / 淡底層 */
--momo-warm-honey: #d6a12f; /* 琥珀金 — 提醒 / 次重點 */
--momo-warm-rust: #c85f6a; /* 玫瑰銅 — 下降 / 風險 */
--momo-warm-mahogany: #a95846; /* 暖陶紅 — 強調 / active */
--momo-warm-earth: #9a8f59; /* 暖橄欖 — 中性成功 / 輔助 */
/* 對應淡色(背景 / 軟標籤用) */
--momo-warm-caramel-soft: rgba(201,100,66,0.12);
--momo-warm-honey-soft: rgba(184,132,22,0.12);
--momo-warm-rust-soft: rgba(181,52,47,0.12);
--momo-warm-mahogany-soft:rgba(143,69,48,0.12);
--momo-warm-earth-soft: rgba(138,90,43,0.12);
--momo-warm-caramel-soft: rgba(217,111,82,0.13);
--momo-warm-peach-soft: rgba(242,159,126,0.18);
--momo-warm-honey-soft: rgba(214,161,47,0.15);
--momo-warm-rust-soft: rgba(200,95,106,0.14);
--momo-warm-mahogany-soft:rgba(169,88,70,0.13);
--momo-warm-earth-soft: rgba(154,143,89,0.15);
/* 頁面調性:共用 shell 與反白 UI 以此套用各頁暖色 accent */
--momo-page-accent: var(--momo-warm-caramel);
@@ -140,7 +142,7 @@
--momo-primary-600: var(--momo-accent-600);
--momo-primary-700: var(--momo-accent-700);
--momo-primary-800: var(--momo-accent-700);
--momo-primary-900: #5e2e20;
--momo-primary-900: #7f3f32;
/* 導航Nothing 黑) */
--momo-nav-start: #1a1a1a;
@@ -154,8 +156,8 @@
--momo-gradient-primary: #1a1a1a;
--momo-gradient-nav: linear-gradient(180deg, #1a1a1a 0%, #000 100%);
--momo-gradient-success: #2a7a3f;
--momo-gradient-danger: #b5342f;
--momo-gradient-warning: #b88416;
--momo-gradient-danger: var(--momo-warm-rust);
--momo-gradient-warning: var(--momo-warm-honey);
--momo-gradient-info: #2d5d80;
--momo-gradient-subtle: linear-gradient(180deg, #f7f5ef 0%, #ebe8e1 100%);
@@ -164,11 +166,11 @@
--momo-success-bg: #e3ebd9;
--momo-success-border: #c5d4b0;
--momo-success-text: #1f5a2d;
--momo-danger: #b5342f;
--momo-danger: var(--momo-warm-rust);
--momo-danger-bg: #f0d8d4;
--momo-danger-border: #d9b1ac;
--momo-danger-text: #7d2520;
--momo-warning: #b88416;
--momo-warning: var(--momo-warm-honey);
--momo-warning-bg: #f3e7c4;
--momo-warning-border: #d9c590;
--momo-warning-text: #6e500e;
@@ -183,8 +185,8 @@
--momo-text-tertiary: #9b9081;
--momo-text-disabled: #c4baa8;
--momo-text-inverse: #faf7f0;
--momo-text-link: #c96442;
--momo-text-link-hover: #8f4530;
--momo-text-link: var(--momo-warm-caramel);
--momo-text-link-hover: var(--momo-warm-mahogany);
--momo-text-strong: var(--momo-text-primary);
--momo-text-muted: var(--momo-text-secondary);
--momo-muted: var(--momo-text-secondary);
@@ -196,7 +198,7 @@
--momo-border-strong: rgba(42,37,32,0.42);
--momo-border-subtle: var(--momo-border-light);
--momo-border-dark: #2a2520;
--momo-border-focus: #c96442;
--momo-border-focus: var(--momo-warm-caramel);
--momo-divider: rgba(42,37,32,0.12);
/* Overlay */