perf: 延後載入業績圖表資源
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-05-18 08:51:09 +08:00
parent 3a779ca075
commit 81f4a0d18a
6 changed files with 264 additions and 121 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.166"
SYSTEM_VERSION = "V10.167"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -393,32 +393,26 @@
{% 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://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>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
{% if not error %}
<script>
window.__DAILY_SALES__ = {
chartData: {{ chart_data | tojson | safe }},
marketing: {
{% if marketing_data and marketing_data.discount %}
discount: {
labels: {{ marketing_data.discount | map(attribute='name') | list | tojson | safe }},
values: {{ marketing_data.discount | map(attribute='revenue') | list | tojson | safe }}
},
{% endif %}
{% if marketing_data and marketing_data.coupon %}
coupon: {
labels: {{ marketing_data.coupon | map(attribute='name') | list | tojson | safe }},
values: {{ marketing_data.coupon | map(attribute='revenue') | list | tojson | safe }}
}
{% endif %}
},
isMonthView: {{ 'true' if is_month_view else 'false' }}
};
</script>
{% set daily_sales_payload = {
'chartData': chart_data,
'marketing': {
'discount': {
'labels': marketing_data.discount | map(attribute='name') | list,
'values': marketing_data.discount | map(attribute='revenue') | list
} if marketing_data and marketing_data.discount else none,
'coupon': {
'labels': marketing_data.coupon | map(attribute='name') | list,
'values': marketing_data.coupon | map(attribute='revenue') | list
} if marketing_data and marketing_data.coupon else none
},
'isMonthView': is_month_view
} %}
<template id="daily-sales-data">{{ daily_sales_payload | tojson }}</template>
<script src="{{ url_for('static', filename='js/page-daily-sales.js') }}"></script>
{% endif %}
{% endblock %}

View File

@@ -107,8 +107,7 @@
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>
<script id="chart-data" type="application/json">{{ chart_data | tojson }}</script>
<template id="chart-data">{{ chart_data | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/page-growth.js') }}"></script>
{% endblock %}

View File

@@ -16,6 +16,7 @@
* system → terra primaryclay/olive/greige/honey/apricot
*/
(function () {
let chartJsLoader = null;
const root = document.documentElement;
const css = (name, fallback) =>
getComputedStyle(root).getPropertyValue(name).trim() || fallback;
@@ -225,6 +226,27 @@
window.Chart = ThemedChart;
}
function loadChartJs() {
if (window.Chart) {
installChartJsTheme();
return Promise.resolve(window.Chart);
}
if (chartJsLoader) return chartJsLoader;
chartJsLoader = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js';
script.async = true;
script.onload = () => {
installChartJsTheme();
resolve(window.Chart);
};
script.onerror = () => reject(new Error('Chart.js 載入失敗'));
document.head.appendChild(script);
});
return chartJsLoader;
}
// ECharts 走精簡版v2 完整 mergeAxis/tuneSeries 保留邏輯但用群組色)
function installEchartsTheme() {
if (!window.echarts || window.echarts.__ewooocThemed) return;
@@ -257,6 +279,7 @@
readPalette,
theme,
tuneChartConfig,
loadChartJs,
installChartJsTheme,
installEchartsTheme
};

View File

@@ -6,13 +6,23 @@
(function () {
'use strict';
if (!window.__DAILY_SALES__) return;
if (typeof Chart === 'undefined') {
console.error('[daily_sales] Chart.js 未載入');
return;
function readDailySalesData() {
const node = document.getElementById('daily-sales-data');
if (!node && window.__DAILY_SALES__) return window.__DAILY_SALES__;
if (!node) return null;
try {
return JSON.parse(node.textContent || '{}');
} catch (error) {
console.error('[daily_sales] chart data parse failed:', error);
return null;
}
}
const dailySalesData = readDailySalesData();
if (!dailySalesData) return;
window.__DAILY_SALES__ = dailySalesData;
let chartsRendered = false;
// -- Palette 讀自 CSS variable (analysis-chart-theme.js 公開) ----------
function cssVar(name, fallback) {
const v = getComputedStyle(document.documentElement)
@@ -39,12 +49,22 @@
muted: cssVar('--momo-text-muted', '#7e6f5c')
};
Chart.defaults.color = palette.muted;
Chart.defaults.borderColor = rgba(palette.muted, 0.18);
Chart.defaults.font.family = "'Noto Sans TC', 'Inter', system-ui, sans-serif";
function loadChartJs() {
if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) {
return window.EwoooCChartTheme.loadChartJs();
}
if (window.Chart) return Promise.resolve(window.Chart);
return Promise.reject(new Error('Chart.js loader unavailable'));
}
function applyChartDefaults() {
Chart.defaults.color = palette.muted;
Chart.defaults.borderColor = rgba(palette.muted, 0.18);
Chart.defaults.font.family = "'Noto Sans TC', 'Inter', system-ui, sans-serif";
}
// -- Safe data extraction ---------------------------------------------
const cd = window.__DAILY_SALES__.chartData || {};
const cd = dailySalesData.chartData || {};
const safe = {
labels: cd.labels || [],
revenue: cd.revenue || [],
@@ -291,7 +311,7 @@
order: [[2, 'desc']],
pageLength: 25,
language: {
url: '//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json'
url: 'https://cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json'
}
});
}
@@ -355,16 +375,45 @@
};
// -- Boot -----------------------------------------------------------
document.addEventListener('DOMContentLoaded', function () {
function renderAllCharts() {
if (chartsRendered) return;
chartsRendered = true;
applyChartDefaults();
renderTrend();
renderDod();
renderWow();
renderTop10();
const mk = window.__DAILY_SALES__.marketing || {};
const mk = dailySalesData.marketing || {};
if (mk.discount) renderMarketingBar('discountChart', mk.discount, palette.caramel);
if (mk.coupon) renderMarketingBar('couponChart', mk.coupon, palette.olive);
}
function bootCharts() {
loadChartJs()
.then(renderAllCharts)
.catch(error => console.error('[daily_sales] Chart.js 載入失敗:', error));
}
function observeCharts() {
const targets = Array.from(document.querySelectorAll('.chart-card'));
if (!targets.length) return;
if (!('IntersectionObserver' in window)) {
bootCharts();
return;
}
const observer = new IntersectionObserver(entries => {
if (!entries.some(entry => entry.isIntersecting)) return;
observer.disconnect();
bootCharts();
}, { rootMargin: '260px 0px' });
targets.forEach(target => observer.observe(target));
}
document.addEventListener('DOMContentLoaded', function () {
initDataTable();
observeCharts();
});
})();

View File

@@ -1,104 +1,182 @@
/* ════════════════════════════════════════════════════════
* page-growth.js — Turn C
* page-growth.js
* growth_analysis.html 的 Chart.js 圖表
* 依賴 analysis-chart-theme.js 統一字型/border/暖色 palette
* Chart.js 由 analysis-chart-theme.js 懶載入,避免首屏阻塞。
* ════════════════════════════════════════════════════════ */
(function () {
'use strict';
const data = JSON.parse(document.getElementById('chart-data').textContent);
let chartsRendered = false;
function readGrowthData() {
const node = document.getElementById('chart-data');
if (!node) return null;
try {
return JSON.parse(node.textContent || '{}');
} catch (error) {
console.error('[growth_analysis] chart data parse failed:', error);
return null;
}
}
const data = readGrowthData();
if (!data) return;
const rootStyle = getComputedStyle(document.documentElement);
const token = (name, fallback) => rootStyle.getPropertyValue(name).trim() || fallback;
// 與 design system page-group=analytics 對齊的暖色 palette
const chartPalette = {
caramel: token('--momo-page-chart-2', '#c96442'),
caramelSoft: token('--momo-warm-caramel-soft', 'rgba(201, 100, 66, 0.58)'),
honey: token('--momo-page-accent', '#c89043'),
honeySoft: token('--momo-page-accent-soft', 'rgba(200, 144, 67, 0.14)'),
rust: token('--momo-danger-text', '#7a3210'),
rustSoft: token('--momo-danger-bg', '#efd3c4')
caramel: token('--momo-page-chart-2', '#c96442'),
caramelSoft: token('--momo-warm-caramel-soft', 'rgba(201, 100, 66, 0.58)'),
honey: token('--momo-page-accent', '#c89043'),
honeySoft: token('--momo-page-accent-soft', 'rgba(200, 144, 67, 0.14)'),
rust: token('--momo-danger-text', '#7a3210'),
rustSoft: token('--momo-danger-bg', '#efd3c4')
};
Chart.defaults.color = token('--momo-text-secondary', '#6b6155');
Chart.defaults.borderColor = token('--momo-border-light', 'rgba(42, 37, 32, 0.10)');
Chart.defaults.font.family = token('--momo-font-family', "'Inter', system-ui, sans-serif");
function loadChartJs() {
if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) {
return window.EwoooCChartTheme.loadChartJs();
}
if (window.Chart) return Promise.resolve(window.Chart);
return Promise.reject(new Error('Chart.js loader unavailable'));
}
// 1) Revenue + YoY
new Chart(document.getElementById('revenueChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{ label: '月營收 ($)', data: data.revenue,
backgroundColor: chartPalette.caramelSoft, order: 2 },
{ label: 'YoY 年增率 (%)', data: data.yoy, type: 'line',
borderColor: chartPalette.rust, borderWidth: 2,
yAxisID: 'y1', order: 1, tension: 0.3 }
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, title: { display: true, text: '金額 ($)' } },
y1: { position: 'right', grid: { drawOnChartArea: false },
title: { display: true, text: '成長率 (%)' } }
function renderCharts() {
if (chartsRendered) return;
chartsRendered = true;
Chart.defaults.color = token('--momo-text-secondary', '#6b6155');
Chart.defaults.borderColor = token('--momo-border-light', 'rgba(42, 37, 32, 0.10)');
Chart.defaults.font.family = token('--momo-font-family', "'Inter', system-ui, sans-serif");
const revenueEl = document.getElementById('revenueChart');
const momEl = document.getElementById('momChart');
const aovEl = document.getElementById('aovChart');
const marginEl = document.getElementById('marginChart');
if (!revenueEl || !momEl || !aovEl || !marginEl) return;
new Chart(revenueEl, {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{
label: '月營收 ($)',
data: data.revenue,
backgroundColor: chartPalette.caramelSoft,
order: 2
},
{
label: 'YoY 年增率 (%)',
data: data.yoy,
type: 'line',
borderColor: chartPalette.rust,
borderWidth: 2,
yAxisID: 'y1',
order: 1,
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, title: { display: true, text: '金額 ($)' } },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
title: { display: true, text: '成長率 (%)' }
}
}
}
}
});
});
// 2) MoM
new Chart(document.getElementById('momChart'), {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'MoM 月增率 (%)',
data: data.mom,
backgroundColor: ctx => ctx.raw >= 0 ? chartPalette.honeySoft : chartPalette.rustSoft
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } }
}
});
new Chart(momEl, {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'MoM 月增率 (%)',
data: data.mom,
backgroundColor: ctx => ctx.raw >= 0 ? chartPalette.honeySoft : chartPalette.rustSoft
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } }
}
});
// 3) AOV
new Chart(document.getElementById('aovChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '平均單價 ($)',
data: data.aov,
borderColor: chartPalette.caramel,
backgroundColor: chartPalette.caramelSoft,
fill: true, tension: 0.4
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
new Chart(aovEl, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '平均單價 ($)',
data: data.aov,
borderColor: chartPalette.caramel,
backgroundColor: chartPalette.caramelSoft,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
// 4) Margin
new Chart(document.getElementById('marginChart'), {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '毛利率 (%)',
data: data.margin_rate,
borderColor: chartPalette.honey,
backgroundColor: chartPalette.honeySoft,
fill: true, tension: 0.4
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
new Chart(marginEl, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '毛利率 (%)',
data: data.margin_rate,
borderColor: chartPalette.honey,
backgroundColor: chartPalette.honeySoft,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
}
function bootCharts() {
loadChartJs()
.then(renderCharts)
.catch(error => console.error('[growth_analysis] Chart.js 載入失敗:', error));
}
function observeCharts() {
const targets = Array.from(document.querySelectorAll('.ga-chart-card'));
if (!targets.length) return;
if (!('IntersectionObserver' in window)) {
bootCharts();
return;
}
});
const observer = new IntersectionObserver(entries => {
if (!entries.some(entry => entry.isIntersecting)) return;
observer.disconnect();
bootCharts();
}, { rootMargin: '220px 0px' });
targets.forEach(target => observer.observe(target));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeCharts, { once: true });
} else {
observeCharts();
}
})();