@@ -5,7 +5,7 @@
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, maximum-scale=1.0" >
< title > {{REPORT_TITLE}} · 动态报表< / title >
< script src = "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" > < / script >
<!-- Chart.js loaded dynamically below -->
< style >
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Inter","PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;min-height:100vh;transition:background .3s,color .3s}
@@ -190,7 +190,6 @@ tr.ch.h{display:none}
< div class = "summary" id = "sumArea" > < / div >
< div class = "report-desc" id = "descArea" > 📊 加载中...< / div >
< div class = "charts-wrap" id = "chartsArea" > < / div >
< div class = "flt" id = "fltArea" >
< div class = "flt-g wide" > < label > 🔍< / label > < input id = "fG" placeholder = "全局搜索..." oninput = "rf()" > < / div >
@@ -213,7 +212,6 @@ tr.ch.h{display:none}
< / div >
< div class = "tb-r" id = "tbRight" style = "display:flex;gap:8px;align-items:center;flex-wrap:wrap" >
< div id = "chartCtrls" style = "display:none;gap:6px;align-items:center;flex-wrap:wrap" >
< span style = "font-size:0.7rem;color:var(--fg4);white-space:nowrap" > 📊 图表< / span >
< select id = "qcDimSel" onchange = "updateQuickChart()" title = "按此字段分组" style = "padding:4px 8px;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;color:var(--fg);font-size:0.75rem;cursor:pointer;font-family:inherit;outline:none" >
< option value = "" > 维度< / option >
< / select >
@@ -226,16 +224,17 @@ tr.ch.h{display:none}
< button class = "btn chart-type-btn" onclick = "setChartType('pie')" id = "btnPie" title = "饼图" > 🥧< / button >
< button class = "btn chart-type-btn" onclick = "setChartType('doughnut')" id = "btnDoughnut" title = "环形图" > 🍩< / button >
< button class = "btn chart-type-btn" onclick = "setChartType('line')" id = "btnLine" title = "折线图" > 📈< / button >
< button class = "btn chart-type-btn" id = "btnList" title = "关闭图表 " onclick = "toggleChart View()" style = "font-size:0.7rem" > 📋< / button >
< button class = "btn chart-type-btn act " id = "btnList" title = "列表视图 " onclick = "showTable View()" style = "font-size:0.7rem" > 📋< / button >
< / div >
< / div >
< button class = "btn-excel" onclick = "expXls()" > ⬇ 导出 Excel< / button >
< / div >
< / div >
< div class = "tw" > < div class = "ts" id = "ts" >
< div class = "tw" id = "tw" > < div class = "ts" id = "ts" >
< table > < thead id = "thd" > < / thead > < tbody id = "tbd" > < / tbody > < / table >
< / div > < div class = "no-result" id = "nr" > < p > 没有匹配的数据< / p > < / div > < / div >
< div class = "charts-wrap" id = "chartsArea" > < / div >
< div class = "ft" >
< div class = "ft-info" id = "ftInfo" > < / div >
@@ -298,15 +297,24 @@ let grpExp={};
let fzCount=0;
let dark=true;
let qcState={dim:'',val:'',type:'bar'};
let chartVisible=true;
let chartVisible=false; // false=table view, true=chart view
function toggle ChartView(){
chartVisible=!chartVisibl e;
const area= $('chartsArea');
const btn=$('btnList') ;
if(area)area .style.display=chartVisible?'': 'none';
if(btn)btn.classList.toggle('act',!chartVisible );
if(chartVisible) renderCharts();
function show ChartView(){
chartVisible=tru e;
$('chartsArea').style.display='' ;
$('tw').style.display='none' ;
$('nr') .style.display='none';
updateChartTypeBtns( );
renderCharts();
}
function showTableView(){
chartVisible=false;
$('chartsArea').style.display='none';
$('tw').style.display='';
// sync active button state
['btnBar','btnPie','btnDoughnut','btnLine','btnList'].forEach(id=>{const b=$(id);if(b)b.classList.remove('act')});
$('btnList').classList.add('act');
}
function init(){
@@ -319,8 +327,25 @@ function init(){
initChartCtrls();
buildChartsUI();
r();
showTableView(); // default: table view
}
init();
// Load Chart.js dynamically, then init — no race conditions
function loadChartJS(onReady){
if(typeof Chart!=='undefined'){onReady();return}
var s=document.createElement('script');
s.src='https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js';
s.onload=onReady;
s.onerror=function(){
var s2=document.createElement('script');
s2.src='https://cdn.bootcdn.net/ajax/libs/Chart.js/4.4.7/chart.umd.min.js';
s2.onload=onReady;
s2.onerror=function(){console.warn('Chart.js failed to load from both CDNs, running without charts');onReady()};
document.head.appendChild(s2);
};
document.head.appendChild(s);
}
loadChartJS(init);
function r(){
const fd=flt();
@@ -355,6 +380,17 @@ function initChartCtrls(){
ctrls.style.display='flex';
const dimSel=$('qcDimSel');
const valSel=$('qcValSel');
// Auto-select defaults: pick best categorical dim (string, few unique vals, not first col)
if(!qcState.dim){
const catCols=COLS.filter((c,i)=>{
if(i===0)return false; // skip ID column
const vals=data.map(r=>String(r[c.k]||''));
const uniq=new Set(vals);
return uniq.size>1&&uniq.size<=15;
});
qcState.dim=catCols.length?catCols[0].k:(COLS[2]?COLS[2].k:COLS[0].k);
}
if(!qcState.val){qcState.val='__count__'}
if(dimSel){
dimSel.innerHTML='< option value = "" > 维度< / option > '+COLS.map(c=>`< option value = "${c.k}" > ${c.l}< / option > `).join('');
dimSel.value=qcState.dim;
@@ -371,14 +407,11 @@ function updateChartTypeBtns(){
const btn=$(`btn${t==='bar'?'Bar':t==='pie'?'Pie':t==='doughnut'?'Doughnut':'Line'}`);
if(btn)btn.classList.toggle('act',qcState.type===t);
});
['btnList'].forEach(id=>{const b=$(id);if(b)b.classList.remove('act')});
}
function setChartType(type){
qcState.type=type;
if(!chartVisible){chartVisible=true;$('chartsArea').style.display='';$('btnList').classList.remove('act')}
updateChartTypeBtns();
renderCharts();
showChartView();
}
function updateQuickChart(){
@@ -460,6 +493,7 @@ function renderCharts(){
// Quick chart mode
const dim=qcState.dim, val=qcState.val, tp=qcState.type;
if(!dim||!val)return;
if(typeof Chart==='undefined'){console.warn('Chart.js not loaded');return}
const groups={};
fd.forEach(r=>{
const k=String(r[dim]||'(空)');
@@ -471,33 +505,35 @@ function renderCharts(){
const wrap=$('qcChartWrap');
if(!wrap)return;
wrap.style.display='block';
wrap.innerHTML='< div class = "chart-box" > < canvas id = "qcCanvas" height = "260 "> < / canvas > < / div > ';
wrap.innerHTML='< div class = "chart-box" > < div class = "chart-title" > '+(COLS.find(c=>c.k===dim)?.l||dim)+' × '+(val==='__count__'?'计数':COLS.find(c=>c.k===val)?.l||val)+' < / div > < div style = "position:relative;height:260px" > < canvas id = "qcCanvas "> < / canvas > < / div > < / div > ';
const canvas=$('qcCanvas');
if(!canvas)return;
const isPie=tp==='pie'||tp==='doughnut'||tp==='polarArea' ;
const cfg= {
type:tp,
data:{
labels,
datasets:[{
label:val==='__count__'?'计数':(COLS.find(c=>c.k===val)?.l||val),
data:values ,
backgroundColor:labels.map((_,j)=>chartColors()[j%chartColors().length]) ,
borderColor:isPie?'transparent': chartColors(),
borderWidth :isPie?0:1 ,
borderRadius :isPie?0:4 ,
}]
},
options:{
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:isPie?{position:'right',labels:{color:chartTextColor(),padding:12,font:{size:11}}}:{display:false}
const isPie=tp==='pie'||tp==='doughnut';
try {
const cfg={
type:tp,
data:{
labels,
datasets:[{
label:val==='__count__'?'计数':(COLS.find(c=>c.k===val)?.l||val) ,
data:values ,
backgroundColor:labels.map((_,j)=>chartColors()[j% chartColors().length]) ,
borderColor :isPie?'transparent':chartColors() ,
borderWidth :isPie?0:1 ,
borderRadius:isPie?0:4,
}]
},
scales:isPie?{}:{x:{grid:{color:chartGridColor()},ticks:{color:chartTextColor(),font:{size:10}}},y:{grid:{color:chartGridColor()},ticks:{color:chartTextColor(),font:{size:10}}}}
}
};
chartInstances.push(new Chart(canvas,cfg));
options:{
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:isPie?{position:'right',labels:{color:chartTextColor(),padding:12,font:{size:11}}}:{display:false}
},
scales:isPie?{}:{x:{grid:{color:chartGridColor()},ticks:{color:chartTextColor(),font:{size:10}}},y:{grid:{color:chartGridColor()},ticks:{color:chartTextColor(),font:{size:10}}}}
}
};
chartInstances.push(new Chart(canvas,cfg));
}catch(e){console.error('Chart render error:',e)}
}
}