diff --git a/config.py b/config.py
index 7395b71..b8cd805 100644
--- a/config.py
+++ b/config.py
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.133"
+SYSTEM_VERSION = "V10.134"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示
diff --git a/web/static/css/page-monthly-summary-bem.css b/web/static/css/page-monthly-summary-bem.css
index f2ee1e6..1cf53f1 100644
--- a/web/static/css/page-monthly-summary-bem.css
+++ b/web/static/css/page-monthly-summary-bem.css
@@ -284,6 +284,149 @@
/* ── 9. 響應式 ─────────────────────────────────────────── */
@media (max-width: 768px) {
- .ms-page-head { grid-template-columns: 1fr; }
- .ms-filter-card { position: static; }
+ .ms-page {
+ gap: var(--momo-space-3);
+ }
+
+ .ms-page-head {
+ grid-template-columns: 1fr;
+ padding: var(--momo-space-4);
+ }
+
+ .ms-page-head__title {
+ align-items: flex-start;
+ font-size: 1.5rem;
+ line-height: 1.2;
+ }
+
+ .ms-page-head__sub,
+ .ms-page-head__sub code {
+ overflow-wrap: anywhere;
+ }
+
+ .ms-filter-card {
+ position: static;
+ padding: var(--momo-space-3);
+ }
+
+ .ms-filter-group::before {
+ top: 30px;
+ bottom: 4px;
+ }
+
+ .ms-filter-card__refresh {
+ min-height: 42px;
+ }
+
+ .ms-chart-card,
+ .ms-special,
+ .ms-data-table {
+ margin-bottom: var(--momo-space-3) !important;
+ }
+
+ .ms-chart-card__head,
+ .ms-special__head,
+ .ms-data-table__head {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: var(--momo-space-2);
+ padding: var(--momo-space-3);
+ }
+
+ .ms-chart-card__title,
+ .ms-special__title,
+ .ms-data-table__title {
+ align-items: flex-start;
+ font-size: var(--momo-text-base);
+ line-height: 1.35;
+ }
+
+ .ms-chart-card .card-body,
+ .ms-special .card-body {
+ overflow: hidden;
+ padding: var(--momo-space-3);
+ }
+
+ .ms-chart-card__canvas {
+ height: 300px !important;
+ min-height: 260px;
+ }
+
+ #vendorRankingChart {
+ height: 620px !important;
+ }
+
+ #divisionDistChart,
+ #priceRangeChart {
+ height: 280px !important;
+ }
+
+ #bcgMatrixChart,
+ #priceVolumeScatterChart,
+ #seasonalityHeatmapChart,
+ #areaRankingChart,
+ #specialChart,
+ #bodyCareChart,
+ #makeupFragranceChart,
+ #privacyInfantChart {
+ height: 320px !important;
+ }
+
+ .ms-chart-card .table-responsive,
+ .ms-chart-card__scroll,
+ #compareChartDataTable,
+ #specialChartDataTable,
+ #bodyCareChartDataTable,
+ #makeupFragranceChartDataTable,
+ #privacyInfantChartDataTable {
+ display: none !important;
+ }
+
+ .ms-data-table__import {
+ justify-content: center;
+ width: 100%;
+ min-height: 40px;
+ }
+
+ .ms-data-table .card-body {
+ padding: var(--momo-space-2);
+ }
+
+ .ms-data-table .table-responsive {
+ border: 1px solid var(--ms-card-border);
+ border-radius: var(--momo-radius-md);
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .ms-data-table__table {
+ min-width: 760px;
+ font-size: var(--momo-text-xs);
+ }
+
+ .ms-data-table__table thead th {
+ font-size: 10px;
+ }
+
+ .dataTables_wrapper .row {
+ gap: var(--momo-space-2);
+ margin-right: 0;
+ margin-left: 0;
+ }
+
+ .dataTables_wrapper .row > [class*="col-"] {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .dataTables_wrapper .dataTables_length,
+ .dataTables_wrapper .dataTables_filter,
+ .dataTables_wrapper .dataTables_info,
+ .dataTables_wrapper .dataTables_paginate {
+ text-align: left !important;
+ }
+
+ .dataTables_wrapper .dataTables_filter label,
+ .dataTables_wrapper .dataTables_filter input {
+ width: 100%;
+ }
}
diff --git a/web/static/js/page-monthly-summary.js b/web/static/js/page-monthly-summary.js
index ad84d2c..d952b1d 100644
--- a/web/static/js/page-monthly-summary.js
+++ b/web/static/js/page-monthly-summary.js
@@ -17,6 +17,11 @@
let specialChart, bodyCareChart, makeupFragranceChart, privacyInfantChart;
let currentFilters = { year: '', month: '', area: '', vendor: '', trade: '' };
+ const compactMediaQuery = window.matchMedia('(max-width: 768px)');
+
+ function isCompactViewport() {
+ return compactMediaQuery.matches;
+ }
$(function () {
compareChart = echarts.init(document.getElementById('compareChart'));
@@ -35,6 +40,8 @@
table = $('#summaryTable').DataTable({
language: { url: '//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json' },
+ autoWidth: false,
+ deferRender: true,
pageLength: 20,
order: [[0, 'desc'], [7, 'desc']],
columnDefs: [
@@ -49,6 +56,12 @@
specialChart, bodyCareChart, makeupFragranceChart, privacyInfantChart]
.forEach(c => c && c.resize());
});
+ const refreshForViewport = () => fetchData();
+ if (compactMediaQuery.addEventListener) {
+ compactMediaQuery.addEventListener('change', refreshForViewport);
+ } else if (compactMediaQuery.addListener) {
+ compactMediaQuery.addListener(refreshForViewport);
+ }
fetchData();
});
@@ -184,6 +197,7 @@
// ── Excel-style 主對比圖 ─────────────────────────────
function renderExcelChart(chart, tableBodyId, trend) {
if (!trend) return;
+ const compact = isCompactViewport();
const years = [...new Set(trend.map(t => t.date.split('/')[0]))].sort().reverse();
const currYear = years[0] || String(new Date().getFullYear());
const prevYear = String(parseInt(currYear) - 1);
@@ -214,20 +228,30 @@
chart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: [currYear, prevYear, 'YOY'], bottom: 0, show: false },
- grid: { left: '80', right: '50', top: '50', bottom: '20' },
- xAxis: { type: 'category', data: months, axisLabel: { show: false }, axisTick: { show: true } },
+ grid: {
+ left: compact ? '42' : '80',
+ right: compact ? '16' : '50',
+ top: compact ? '26' : '50',
+ bottom: compact ? '28' : '20'
+ },
+ xAxis: {
+ type: 'category',
+ data: months,
+ axisLabel: { show: compact, interval: 0, fontSize: 10 },
+ axisTick: { show: true }
+ },
yAxis: [
- { type: 'value', name: '業績', axisLabel: { formatter: v => v.toLocaleString() }, splitLine: { lineStyle: { type: 'dashed' } } },
- { type: 'value', name: 'YoY', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } }
+ { type: 'value', name: compact ? '' : '業績', axisLabel: { formatter: v => compact ? (v/10000).toFixed(0) + '萬' : v.toLocaleString(), fontSize: compact ? 10 : 12 }, splitLine: { lineStyle: { type: 'dashed' } } },
+ { type: 'value', name: compact ? '' : 'YoY', axisLabel: { formatter: '{value}%', fontSize: compact ? 10 : 12 }, splitLine: { show: false } }
],
series: [
- { name: currYear, type: 'bar', data: currData, barMaxWidth: 20,
- label: { show: true, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? (p.value/10000).toFixed(0) + '萬' : '' } },
- { name: prevYear, type: 'bar', data: prevData, barMaxWidth: 20,
- label: { show: true, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? (p.value/10000).toFixed(0) + '萬' : '' } },
+ { name: currYear, type: 'bar', data: currData, barMaxWidth: compact ? 12 : 20,
+ label: { show: !compact, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? (p.value/10000).toFixed(0) + '萬' : '' } },
+ { name: prevYear, type: 'bar', data: prevData, barMaxWidth: compact ? 12 : 20,
+ label: { show: !compact, position: 'top', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? (p.value/10000).toFixed(0) + '萬' : '' } },
{ name: 'YOY', type: 'line', yAxisIndex: 1, data: yoyData, smooth: true,
- lineStyle: { width: 2 }, symbol: 'circle', symbolSize: 6,
- label: { show: true, position: 'bottom', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? p.value + '%' : '' } }
+ lineStyle: { width: compact ? 1.5 : 2 }, symbol: 'circle', symbolSize: compact ? 4 : 6,
+ label: { show: !compact, position: 'bottom', fontSize: 16, fontWeight: 'bold', formatter: p => p.value ? p.value + '%' : '' } }
]
});
}
@@ -235,6 +259,7 @@
// ── YoY Trend ─────────────────────────────────────────
function renderYoYTrendChart(chart, tableId, data) {
if (!data || !data.length) return;
+ const compact = isCompactViewport();
const dates = data.map(d => d.date.split('/')[1] + '月');
const currData = data.map(d => d.curr);
const yoaData = data.map(d => d.yoa);
@@ -249,10 +274,10 @@
chart.setOption({
tooltip: { trigger: 'axis' },
- grid: { left: '60', right: '30', top: '40', bottom: '20' },
- legend: { data: [currYearStr, '去年同期'], bottom: 0 },
- xAxis: { type: 'category', data: dates },
- yAxis: { type: 'value', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬' } },
+ grid: { left: compact ? '42' : '60', right: compact ? '14' : '30', top: compact ? '24' : '40', bottom: compact ? '26' : '20' },
+ legend: { data: [currYearStr, '去年同期'], bottom: 0, show: !compact },
+ xAxis: { type: 'category', data: dates, axisLabel: { fontSize: compact ? 10 : 12 } },
+ yAxis: { type: 'value', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: compact ? 10 : 12 } },
series: [
{ name: currYearStr, type: 'line', data: currData, lineStyle: { width: 3 }, showSymbol: false, areaStyle: { opacity: 0.12 } },
{ name: '去年同期', type: 'line', data: yoaData, lineStyle: { type: 'dashed' }, showSymbol: false }
@@ -263,19 +288,20 @@
// ── 類別分佈 (雙圓餅) ─────────────────────────────────
function renderDivisionDistChart(chart, _tableId, data) {
if (!data || !data.length) return;
+ const compact = isCompactViewport();
const pie = key => data.map(d => ({ name: d.name, value: d[key] || 0 })).filter(d => d.value > 0);
chart.setOption({
title: [
- { text: '2024 年', left: '23%', top: '5%', textAlign: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } },
- { text: '2025 年', left: '73%', top: '5%', textAlign: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } }
+ { text: '2024 年', left: '23%', top: compact ? '2%' : '5%', textAlign: 'center', textStyle: { fontSize: compact ? 12 : 16, fontWeight: 'bold' } },
+ { text: '2025 年', left: '73%', top: compact ? '2%' : '5%', textAlign: 'center', textStyle: { fontSize: compact ? 12 : 16, fontWeight: 'bold' } }
],
tooltip: { trigger: 'item', formatter: p => `${p.name}
業績: ${(p.value/10000).toFixed(0)}萬
佔比: ${p.percent.toFixed(1)}%` },
- legend: { type: 'scroll', orient: 'horizontal', bottom: 0, textStyle: { fontSize: 12, fontWeight: 'bold' } },
+ legend: { type: 'scroll', orient: 'horizontal', bottom: 0, show: !compact, textStyle: { fontSize: 12, fontWeight: 'bold' } },
series: [
- { name: '2024 年', type: 'pie', radius: ['30%', '60%'], center: ['25%', '55%'], data: pie('sales_2024'),
- label: { show: true, fontSize: 11, fontWeight: 'bold', formatter: p => `${p.name}\n${(p.value/10000).toFixed(0)}萬\n${p.percent.toFixed(1)}%` } },
- { name: '2025 年', type: 'pie', radius: ['30%', '60%'], center: ['75%', '55%'], data: pie('sales_2025'),
- label: { show: true, fontSize: 11, fontWeight: 'bold', formatter: p => `${p.name}\n${(p.value/10000).toFixed(0)}萬\n${p.percent.toFixed(1)}%` } }
+ { name: '2024 年', type: 'pie', radius: compact ? ['36%', '58%'] : ['30%', '60%'], center: ['25%', compact ? '53%' : '55%'], data: pie('sales_2024'),
+ label: { show: !compact, fontSize: 11, fontWeight: 'bold', formatter: p => `${p.name}\n${(p.value/10000).toFixed(0)}萬\n${p.percent.toFixed(1)}%` } },
+ { name: '2025 年', type: 'pie', radius: compact ? ['36%', '58%'] : ['30%', '60%'], center: ['75%', compact ? '53%' : '55%'], data: pie('sales_2025'),
+ label: { show: !compact, fontSize: 11, fontWeight: 'bold', formatter: p => `${p.name}\n${(p.value/10000).toFixed(0)}萬\n${p.percent.toFixed(1)}%` } }
]
});
}
@@ -283,6 +309,7 @@
// ── 價格帶 ────────────────────────────────────────────
function renderPriceRangeChart(chart, tableId, data) {
if (!data || !data.length) return;
+ const compact = isCompactViewport();
const order = ['0-499','500-999','1,000-1,999','2,000-4,999','5,000-9,999','10,000+'];
data.sort((a,b) => order.indexOf(a.range) - order.indexOf(b.range));
const t24 = data.reduce((s, x) => s + (x.sales_2024 || 0), 0);
@@ -297,15 +324,15 @@
return `${p.marker} ${p.seriesName}: ${v.toLocaleString()} (${pct})`;
}).join('
')
},
- legend: { data: ['2024 銷售', '2025 銷售'], bottom: 0, textStyle: { fontSize: 13, fontWeight: 'bold' } },
- grid: { left: '3%', right: '4%', bottom: '12%', top: '15%', containLabel: true },
- xAxis: { type: 'category', data: data.map(d => d.range), axisLabel: { interval: 0, fontSize: 13, fontWeight: 'bold' } },
- yAxis: { type: 'value', name: '銷售額', nameTextStyle: { fontSize: 12, fontWeight: 'bold' },
- axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: 12 } },
+ legend: { data: ['2024 銷售', '2025 銷售'], bottom: 0, show: !compact, textStyle: { fontSize: 13, fontWeight: 'bold' } },
+ grid: { left: compact ? '2%' : '3%', right: compact ? '2%' : '4%', bottom: compact ? '18%' : '12%', top: compact ? '8%' : '15%', containLabel: true },
+ xAxis: { type: 'category', data: data.map(d => d.range), axisLabel: { interval: 0, rotate: compact ? 32 : 0, fontSize: compact ? 10 : 13, fontWeight: 'bold' } },
+ yAxis: { type: 'value', name: compact ? '' : '銷售額', nameTextStyle: { fontSize: 12, fontWeight: 'bold' },
+ axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: compact ? 10 : 12 } },
series: ['2024','2025'].map((y) => ({
- name: y + ' 銷售', type: 'bar', barWidth: 30,
+ name: y + ' 銷售', type: 'bar', barWidth: compact ? 13 : 30,
data: data.map(d => d['sales_' + y] || 0),
- label: { show: true, position: 'top', fontSize: 12, fontWeight: 'bold',
+ label: { show: !compact, position: 'top', fontSize: 12, fontWeight: 'bold',
formatter: p => {
const v = p.value || 0;
const total = y === '2024' ? t24 : t25;
@@ -321,19 +348,20 @@
// ── BCG 矩陣(4 象限顯式配色) ───────────────────────
function renderBCGMatrixChart(chart, data) {
if (!data || !data.length) return;
+ const compact = isCompactViewport();
const C_STAR = 'var(--momo-warm-olive, #6f7a4a)';
const C_COW = 'var(--momo-warm-honey, #c89043)';
const C_QUESTION = 'var(--momo-warm-caramel, #c96442)';
const C_DOG = 'var(--momo-warm-mahogany, #7a3b2c)';
chart.setOption({
tooltip: { formatter: p => `