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 => `
${p.value[3]}
毛利率: ${p.value[0]}%
銷售額: $${p.value[1].toLocaleString()}
銷量: ${p.value[2].toLocaleString()}
` }, - grid: { left: '80', right: '50', top: '30', bottom: '30' }, - xAxis: { name: '毛利率(%)', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } }, - yAxis: { name: '業績($)', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } }, + grid: { left: compact ? '46' : '80', right: compact ? '16' : '50', top: compact ? '18' : '30', bottom: compact ? '34' : '30' }, + xAxis: { name: compact ? '' : '毛利率(%)', type: 'value', axisLabel: { fontSize: compact ? 10 : 12 }, splitLine: { lineStyle: { type: 'dashed' } } }, + yAxis: { name: compact ? '' : '業績($)', type: 'value', axisLabel: { formatter: v => compact ? (v/10000).toFixed(0) + '萬' : v.toLocaleString(), fontSize: compact ? 10 : 12 }, splitLine: { lineStyle: { type: 'dashed' } } }, series: [{ type: 'scatter', data: data.map(d => [d.margin, d.sales, d.qty, d.name]), - symbolSize: d => Math.sqrt(d[2]) * 1.5, + symbolSize: d => Math.min(compact ? 52 : 96, Math.max(compact ? 8 : 10, Math.sqrt(Math.max(d[2], 1)) * (compact ? 0.35 : 0.75))), itemStyle: { color: p => { if (p.value[0] >= 30 && p.value[1] >= 500000) return C_STAR; @@ -344,8 +372,8 @@ opacity: 0.78, borderColor: '#fff8ef', borderWidth: 1 }, markLine: { silent: true, data: [ - { xAxis: 30, lineStyle: { color: '#999' }, label: { formatter: '高獲利線 (30%)' } }, - { yAxis: 500000, lineStyle: { color: '#999' }, label: { formatter: '高營收線 ($50萬)' } } + { xAxis: 30, lineStyle: { color: '#999' }, label: { show: !compact, formatter: '高獲利線 (30%)' } }, + { yAxis: 500000, lineStyle: { color: '#999' }, label: { show: !compact, formatter: '高營收線 ($50萬)' } } ] } }] }); @@ -354,35 +382,37 @@ // ── 散佈圖(價/量) ────────────────────────────────── function renderScatterChart(chart, data) { if (!data || !data.length) return; + const compact = isCompactViewport(); const series = data.map(d => { const avg = d.qty > 0 ? Math.round(d.sales / d.qty) : 0; return [avg, d.qty, d.sales, d.name]; }); chart.setOption({ tooltip: { formatter: p => `
${p.value[3]}
均價: $${p.value[0]}
銷量: ${p.value[1].toLocaleString()}
業績: $${p.value[2].toLocaleString()}
` }, - grid: { left: '60', right: '50', top: '30', bottom: '30' }, - xAxis: { name: '均價($)', type: 'value', splitLine: { show: false } }, - yAxis: { name: '銷量', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } }, - series: [{ type: 'scatter', data: series, symbolSize: d => Math.log(Math.max(d[2],1)) * 3, itemStyle: { opacity: 0.65 } }] + grid: { left: compact ? '42' : '60', right: compact ? '16' : '50', top: compact ? '18' : '30', bottom: compact ? '32' : '30' }, + xAxis: { name: compact ? '' : '均價($)', type: 'value', axisLabel: { fontSize: compact ? 10 : 12 }, splitLine: { show: false } }, + yAxis: { name: compact ? '' : '銷量', type: 'value', axisLabel: { fontSize: compact ? 10 : 12 }, splitLine: { lineStyle: { type: 'dashed' } } }, + series: [{ type: 'scatter', data: series, symbolSize: d => Math.min(compact ? 36 : 64, Math.max(compact ? 6 : 8, Math.log(Math.max(d[2],1)) * (compact ? 1.35 : 2))), itemStyle: { opacity: 0.65 } }] }); } // ── 廠商排行 ────────────────────────────────────────── function renderVendorRankingChart(chart, tableId, data) { if (!data || !data.length) return; + const compact = isCompactViewport(); const cd = data.slice().reverse(); chart.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, - legend: { data: ['2024 銷售額', '2025 銷售額'], bottom: 0, textStyle: { fontSize: 15, fontWeight: 'bold' } }, - grid: { left: '160', right: '110', top: '20', bottom: '45' }, - xAxis: { type: 'value', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: 14, fontWeight: 'bold' } }, - yAxis: { type: 'category', data: cd.map(d => d.name), axisLabel: { width: 150, overflow: 'truncate', interval: 0, fontSize: 14, fontWeight: 'bold' } }, + legend: { data: ['2024 銷售額', '2025 銷售額'], bottom: 0, show: !compact, textStyle: { fontSize: 15, fontWeight: 'bold' } }, + grid: { left: compact ? '86' : '160', right: compact ? '18' : '110', top: compact ? '10' : '20', bottom: compact ? '28' : '45' }, + xAxis: { type: 'value', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: compact ? 10 : 14, fontWeight: 'bold' } }, + yAxis: { type: 'category', data: cd.map(d => d.name), axisLabel: { width: compact ? 78 : 150, overflow: 'truncate', interval: 0, fontSize: compact ? 10 : 14, fontWeight: 'bold' } }, series: [ - { name: '2024 銷售額', type: 'bar', data: cd.map(d => d.sales_2024 || 0), barWidth: 18, barGap: '0%', barCategoryGap: '75%', - label: { show: true, position: 'insideRight', fontSize: 13, fontWeight: 'bold', color: '#fff8ef', textShadowColor: 'rgba(42,37,32,0.5)', textShadowBlur: 2, + { name: '2024 銷售額', type: 'bar', data: cd.map(d => d.sales_2024 || 0), barWidth: compact ? 10 : 18, barGap: '0%', barCategoryGap: compact ? '70%' : '75%', + label: { show: !compact, position: 'insideRight', fontSize: 13, fontWeight: 'bold', color: '#fff8ef', textShadowColor: 'rgba(42,37,32,0.5)', textShadowBlur: 2, formatter: p => p.value > 0 ? (p.value/10000).toFixed(0) + '萬' : '' } }, - { name: '2025 銷售額', type: 'bar', data: cd.map(d => d.sales_2025 || 0), barWidth: 18, - label: { show: true, position: 'right', fontSize: 14, fontWeight: 'bold', + { name: '2025 銷售額', type: 'bar', data: cd.map(d => d.sales_2025 || 0), barWidth: compact ? 10 : 18, + label: { show: !compact, position: 'right', fontSize: 14, fontWeight: 'bold', formatter: p => p.value > 0 ? (p.value/10000).toFixed(0) + '萬' : '' } } ] }); @@ -417,6 +447,7 @@ // ── 季節熱力圖 ──────────────────────────────────────── function renderSeasonalityHeatmap(chart, data) { if (!data || !data.length) return; + const compact = isCompactViewport(); const months = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; const areaSales = {}; data.forEach(d => { areaSales[d.category] = (areaSales[d.category] || 0) + d.sales; }); @@ -432,13 +463,13 @@ chart.setOption({ tooltip: { position: 'top', formatter: p => `${yLabels[p.value[1]]}
${months[p.value[0]]}
業績: ${(p.value[2]/10000).toFixed(0)}萬` }, - grid: { height: '75%', top: '5%', bottom: '18%', left: '15%', right: '3%' }, - xAxis: { type: 'category', data: months, splitArea: { show: true }, axisLabel: { fontSize: 14, fontWeight: 'bold' } }, - yAxis: { type: 'category', data: yLabels, splitArea: { show: true }, axisLabel: { fontSize: 14, fontWeight: 'bold' } }, - visualMap: { min: 0, max, calculable: true, orient: 'horizontal', left: 'center', bottom: '0%', textStyle: { fontSize: 12 }, + grid: { height: compact ? '72%' : '75%', top: compact ? '3%' : '5%', bottom: compact ? '20%' : '18%', left: compact ? '72' : '15%', right: compact ? '5' : '3%' }, + xAxis: { type: 'category', data: months, splitArea: { show: true }, axisLabel: { fontSize: compact ? 10 : 14, fontWeight: 'bold' } }, + yAxis: { type: 'category', data: yLabels, splitArea: { show: true }, axisLabel: { width: compact ? 66 : 120, overflow: 'truncate', fontSize: compact ? 10 : 14, fontWeight: 'bold' } }, + visualMap: { min: 0, max, calculable: true, orient: 'horizontal', left: 'center', bottom: '0%', textStyle: { fontSize: compact ? 10 : 12 }, inRange: { color: ['#fef3c7', '#f5c98a', '#e3a560', '#c96442', '#7a3b2c'] } }, series: [{ type: 'heatmap', data: heatmap, - label: { show: true, fontSize: 12, fontWeight: 'bold', formatter: p => (p.value[2]/10000).toFixed(0) + '萬' }, + label: { show: !compact, fontSize: 12, fontWeight: 'bold', formatter: p => (p.value[2]/10000).toFixed(0) + '萬' }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.45)' } } }] }); } @@ -446,17 +477,18 @@ // ── 區域排行 ────────────────────────────────────────── function renderAreaRankingChart(chart, tableId, data) { if (!data || !data.length) return; + const compact = isCompactViewport(); data.sort((a,b) => b.sales - a.sales); chart.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: ps => ps[0].name + '
' + ps.map(p => `${p.marker} ${p.seriesName}: ${(p.value || 0).toLocaleString()}`).join('
') }, - legend: { data: ['2024 業績', '2025 業績'], bottom: 0, textStyle: { fontSize: 14 } }, - grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true }, - xAxis: { type: 'category', data: data.map(d => d.name), axisLabel: { interval: 0, fontSize: 14, fontWeight: 'bold' } }, - yAxis: { type: 'value', name: '業績', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: 13 } }, + legend: { data: ['2024 業績', '2025 業績'], bottom: 0, show: !compact, textStyle: { fontSize: 14 } }, + grid: { left: compact ? '2%' : '3%', right: compact ? '2%' : '4%', bottom: compact ? '20%' : '10%', top: compact ? '8%' : '10%', containLabel: true }, + xAxis: { type: 'category', data: data.map(d => d.name), axisLabel: { interval: 0, rotate: compact ? 28 : 0, fontSize: compact ? 10 : 14, fontWeight: 'bold' } }, + yAxis: { type: 'value', name: compact ? '' : '業績', axisLabel: { formatter: v => (v/10000).toFixed(0) + '萬', fontSize: compact ? 10 : 13 } }, series: ['2024','2025'].map(y => ({ name: y + ' 業績', type: 'bar', data: data.map(d => d['sales_' + y] || 0), - label: { show: true, position: 'top', fontSize: 14, fontWeight: 'bold', formatter: p => (p.value/10000).toFixed(0) + '萬' } + label: { show: !compact, position: 'top', fontSize: 14, fontWeight: 'bold', formatter: p => (p.value/10000).toFixed(0) + '萬' } })) }); let html = `排名區名稱業績`;