diff --git a/config.py b/config.py
index d47f640..782aca6 100644
--- a/config.py
+++ b/config.py
@@ -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 # 用於模板顯示
diff --git a/templates/daily_sales.html b/templates/daily_sales.html
index 800abc4..540ceba 100644
--- a/templates/daily_sales.html
+++ b/templates/daily_sales.html
@@ -393,32 +393,26 @@
{% endblock %}
{% block extra_js %}
-
{% if not error %}
-
+ {% 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
+ } %}
+ {{ daily_sales_payload | tojson }}
{% endif %}
{% endblock %}
diff --git a/templates/growth_analysis.html b/templates/growth_analysis.html
index 70210e9..7137b05 100644
--- a/templates/growth_analysis.html
+++ b/templates/growth_analysis.html
@@ -107,8 +107,7 @@
{% endblock %}
{% block extra_js %}
-
-
+ {{ chart_data | tojson }}
{% endblock %}
diff --git a/web/static/js/analysis-chart-theme.js b/web/static/js/analysis-chart-theme.js
index c07bebb..b8c3be8 100644
--- a/web/static/js/analysis-chart-theme.js
+++ b/web/static/js/analysis-chart-theme.js
@@ -16,6 +16,7 @@
* system → terra primary,clay/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
};
diff --git a/web/static/js/page-daily-sales.js b/web/static/js/page-daily-sales.js
index 759a905..e8c773f 100644
--- a/web/static/js/page-daily-sales.js
+++ b/web/static/js/page-daily-sales.js
@@ -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();
});
})();
diff --git a/web/static/js/page-growth.js b/web/static/js/page-growth.js
index 2dc48f1..3a02ef2 100644
--- a/web/static/js/page-growth.js
+++ b/web/static/js/page-growth.js
@@ -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();
+ }
})();