看板增加ECharts图表(自托管): 近30天趋势折线/级别环形/学历柱状/期望城市TOP8/筛选漏斗; 统计接口扩充

This commit is contained in:
mzl92
2026-08-27 14:37:06 +08:00
parent 06a907acbf
commit 6e9470a980
4 changed files with 107 additions and 10 deletions
+45
View File
File diff suppressed because one or more lines are too long
+55 -7
View File
@@ -49,6 +49,8 @@ tr:hover td{background:#f8fafc;cursor:pointer}
.bars .bar.g{background:var(--ok)}
.bars .lbl{font-size:11px;color:var(--sub)}
.hidden{display:none}
.chartRow{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px;margin-bottom:16px}
.chartRow .panel{margin-bottom:0}
#toast{position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;padding:8px 18px;border-radius:8px;z-index:9;opacity:.95}
#detailModal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:8;display:flex;align-items:center;justify-content:center}
#detailModal.hidden{display:none}
@@ -76,7 +78,13 @@ tr:hover td{background:#f8fafc;cursor:pointer}
</div>
<div id="main" class="hidden">
<div class="cards" id="statCards"></div>
<div class="panel"><h3>近7天趋势(绿=打招呼)</h3><div class="bars" id="trend"></div></div>
<div class="panel"><h3>近30天趋势</h3><div id="chTrend" style="height:300px"></div></div>
<div class="chartRow">
<div class="panel"><h3>评分级别分布</h3><div id="chLevel" style="height:280px"></div></div>
<div class="panel"><h3>学历分布</h3><div id="chEdu" style="height:280px"></div></div>
<div class="panel"><h3>期望城市 TOP8</h3><div id="chCity" style="height:280px"></div></div>
<div class="panel"><h3>筛选漏斗</h3><div id="chFunnel" style="height:280px"></div></div>
</div>
<div class="panel">
<h3>候选人</h3>
<div class="filters">
@@ -119,6 +127,7 @@ tr:hover td{background:#f8fafc;cursor:pointer}
<div id="detailModal" onclick="if(event.target===this)this.classList.add('hidden')" class="hidden">
<div class="panel detail" style="width:820px;max-width:94vw;max-height:88vh;overflow:auto" id="detailBody"></div>
</div>
<script src="echarts.min.js"></script>
<script>
let me=null,pageCur=1,pageTotal=1;
const $=id=>document.getElementById(id);
@@ -164,13 +173,52 @@ async function loadStats(){
<div class="card"><div class="k">累计打招呼</div><div class="v">${s.greetedTotal}</div></div>
${s.byLevel.map(x=>`<div class="card"><div class="k">${esc(x.k)}</div><div class="v">${x.c}</div></div>`).join('')}
<div class="card"><div class="k">数据快照</div><div class="v" style="font-size:16px">${s.snapshotAt?new Date(new Date(s.snapshotAt).getTime()+8*3600e3).toISOString().slice(11,19):'-'}<span style="font-size:11px;color:var(--sub)"> +8 每5分钟</span></div></div>`;
const mx=Math.max(1,...s.last7.map(d=>d.c));
$('trend').innerHTML=s.last7.map(d=>`<div class="b">
<div class="lbl">${d.c}人/${d.greeted}招呼</div>
<div class="bar" style="height:${d.c/mx*100}%"></div>
<div class="bar g" style="height:${d.greeted/mx*100}%;width:24%"></div>
<div class="lbl">${d.d.slice(5)}</div></div>`).join('');
renderCharts(s);
}
// ---------- 图表(ECharts 自托管) ----------
const charts=[];
function mkChart(id){const c=echarts.init($(id));charts.push(c);return c}
function renderCharts(s){
if(typeof echarts==='undefined'){['chTrend','chLevel','chEdu','chCity','chFunnel'].forEach(id=>{$(id).innerHTML='<div style="color:var(--sub);padding:40px;text-align:center">图表库未加载</div>'});return}
charts.splice(0).forEach(c=>c.dispose());
const axis='#6b7280',split='#e5e7eb';
mkChart('chTrend').setOption({
tooltip:{trigger:'axis'},legend:{data:['新增','推荐','打招呼']},
grid:{left:50,right:20,top:40,bottom:30},
xAxis:{type:'category',data:s.last30.map(d=>d.d.slice(5)),axisLine:{lineStyle:{color:split}},axisLabel:{color:axis}},
yAxis:{type:'value',axisLabel:{color:axis},splitLine:{lineStyle:{color:split}}},
series:[
{name:'新增',type:'line',smooth:true,data:s.last30.map(d=>d.c),areaStyle:{opacity:.15},itemStyle:{color:'#2563eb'}},
{name:'推荐',type:'line',smooth:true,data:s.last30.map(d=>d.rec),itemStyle:{color:'#16a34a'}},
{name:'打招呼',type:'line',smooth:true,data:s.last30.map(d=>d.greeted),itemStyle:{color:'#d97706'}}]});
const lvColor={'推荐':'#16a34a','待定':'#d97706','不推荐':'#9ca3af','不相关':'#dc2626','?':'#9ca3af'};
mkChart('chLevel').setOption({
tooltip:{trigger:'item',formatter:'{b}: {c} ({d}%)'},legend:{bottom:0},
series:[{type:'pie',radius:['42%','68%'],center:['50%','45%'],
data:s.byLevel.map(x=>({name:x.k,value:x.c,itemStyle:{color:lvColor[x.k]||'#9ca3af'}})),
label:{formatter:'{b}\n{c}'}}]});
mkChart('chEdu').setOption({
tooltip:{},grid:{left:40,right:20,top:20,bottom:30},
xAxis:{type:'category',data:s.byEducation.map(x=>x.k),axisLabel:{color:axis,interval:0},axisLine:{lineStyle:{color:split}}},
yAxis:{type:'value',axisLabel:{color:axis},splitLine:{lineStyle:{color:split}}},
series:[{type:'bar',barWidth:'55%',data:s.byEducation.map(x=>x.c),itemStyle:{color:'#2563eb',borderRadius:[4,4,0,0]}}]});
mkChart('chCity').setOption({
tooltip:{},grid:{left:70,right:30,top:20,bottom:30},
xAxis:{type:'value',axisLabel:{color:axis},splitLine:{lineStyle:{color:split}}},
yAxis:{type:'category',data:s.byCity.map(x=>x.k).reverse(),axisLabel:{color:axis}},
series:[{type:'bar',barWidth:'55%',data:s.byCity.map(x=>x.c).reverse(),itemStyle:{color:'#0ea5e9',borderRadius:[0,4,4,0]}}]});
const lv=s.byLevel.reduce((m,x)=>(m[x.k]=x.c,m),{});
mkChart('chFunnel').setOption({
tooltip:{trigger:'item',formatter:'{b}: {c}'},
series:[{type:'funnel',left:'10%',width:'80%',top:10,bottom:10,gap:2,
label:{formatter:'{b} {c}'},
data:[
{name:'累计抓取',value:s.total,itemStyle:{color:'#94a3b8'}},
{name:'过门槛',value:s.total-(lv['不相关']||0),itemStyle:{color:'#2563eb'}},
{name:'推荐+待定',value:(lv['推荐']||0)+(lv['待定']||0),itemStyle:{color:'#d97706'}},
{name:'已打招呼',value:s.greetedTotal,itemStyle:{color:'#16a34a'}}]}]});
}
window.addEventListener('resize',()=>charts.forEach(c=>c.resize()));
function qs(){return `?level=${$('q_level').value}&greeted=${$('q_greeted').value}&account=${$('q_account').value}&date_from=${$('q_from').value}&date_to=${$('q_to').value}&q=${encodeURIComponent($('q_kw').value.trim())}`}
async function loadList(p){
pageCur=p||pageCur;