resumes.html 10.7 KB
{% extends "base.html" %}
{% block title %}简历筛选{% endblock %}

{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.rbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center;}
.rbar input{flex:1;min-width:120px;padding:10px;border:1px solid #ddd;border-radius:8px;font-size:14px;}
.rbar select{padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;}
.rbar button{padding:10px 14px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;}
.rbar button.active{background:#3498db;color:white;border-color:#3498db;}
.rtable{width:100%;border-collapse:collapse;background:white;border-radius:8px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,0.05);}
.rtable th{background:#f8f9fa;text-align:left;padding:10px 12px;font-size:12px;color:#999;font-weight:500;white-space:nowrap;}
.rtable td{padding:10px 12px;border-top:1px solid #f0f0f0;font-size:13px;}
.rtable tr{cursor:pointer;transition:.15s;}
.rtable tbody tr:hover{background:#f5f7fa;}
.rtable tr a{color:inherit;text-decoration:none;display:contents;}
.tier-badge{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500;}
.tier-recommend{background:#d4edda;color:#155724;}
.tier-backup{background:#fff3cd;color:#856404;}
.tier-reject{background:#f8d7da;color:#721c24;}
.tier-eliminated{background:#e2e3e5;color:#383d3d;text-decoration:line-through;}
.tier-unscored{background:#e2e3e5;color:#6c757d;}
.score-bar{width:40px;height:6px;background:#eee;border-radius:3px;display:inline-block;vertical-align:middle;margin-left:4px;}
.score-fill{height:100%;border-radius:3px;}
.score-high{background:#27ae60;}
.score-mid{background:#f39c12;}
.score-low{background:#e74c3c;}
.pagination{display:flex;gap:6px;justify-content:center;margin:16px 0;flex-wrap:wrap;align-items:center;}
.pagination button{padding:8px 12px;border:1px solid #ddd;border-radius:6px;background:white;font-size:13px;cursor:pointer;}
.pagination button.active{background:#3498db;color:white;border-color:#3498db;}
.pagination button:disabled{opacity:.4;cursor:default;}
.pagination span{font-size:13px;color:#666;}
.score-modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:300;align-items:center;justify-content:center;}
.score-modal.show{display:flex;}
.score-modal .modal-box{background:white;border-radius:12px;padding:24px;max-width:420px;width:90%;text-align:center;}
.score-modal .modal-box h3{font-size:18px;margin-bottom:12px;}
.score-modal .modal-box p{font-size:14px;color:#666;margin-bottom:20px;}
.score-modal .modal-box .spinner{margin:20px auto;}
</style>
<div style="max-width:900px;margin:0 auto;padding:20px;">
<h2 style="margin-bottom:12px;">📄 简历筛选</h2>
<div class="rbar">
  <input type="text" id="searchInput" placeholder="搜索姓名/专业/学校..." oninput="render()">
  <select id="jobFilter" onchange="render()">
    <option value="">全部岗位</option>
  </select>
  <select id="tierFilter" onchange="render()">
    <option value="">全部</option>
    <option value="recommend">推荐面试</option>
    <option value="backup">可备选</option>
    <option value="reject">暂不考虑</option>
    <option value="eliminated">已淘汰</option>
    <option value="unscored">未打分</option>
  </select>
  <select id="sortBy" onchange="render()">
    <option value="score">按分数↓</option>
    <option value="name">按姓名</option>
    <option value="edu">按学历</option>
  </select>
  <button id="btnScore" onclick="runScoring()" style="border:1px solid #e67e22;color:#e67e22;background:white;white-space:nowrap;">🎯 筛选打分</button>
</div>
<div style="font-size:12px;color:#999;margin-bottom:12px;" id="infoBar">加载中...</div>
<div id="resumeList"><div class="loading"><div class="spinner"></div></div></div>
<div id="pager"></div>
</div>

<div class="score-modal" id="scoreModal">
  <div class="modal-box">
    <h3>🎯 简历打分中</h3>
    <p id="scoreStatus">正在对未打分的简历进行打分...</p>
    <div class="spinner"></div>
  </div>
</div>

<script>
var allResumes=[],currentPage=1,perPage=20,totalCount=0;
var jobOptions=[];

async function loadData(page){
  currentPage=page;
  document.getElementById('resumeList').innerHTML='<div class="loading"><div class="spinner"></div></div>';
  try{
    var r=await fetch('/api/resumes?page='+page+'&per_page='+perPage);
    var j=await r.json();
    if(!j.ok){document.getElementById('resumeList').innerHTML='<div class="empty-state">加载失败</div>';return;}
    allResumes=j.data;
    totalCount=j.total;
    if(!jobOptions.length){
      var jobs={};
      for(var i=0;i<allResumes.length;i++){var jp=allResumes[i].jobPosition;if(jp&&!jobs[jp])jobs[jp]=1;}
      jobOptions=Object.keys(jobs);
      var sel=document.getElementById('jobFilter');
      for(var k=0;k<jobOptions.length;k++)sel.innerHTML+='<option value="'+esc(jobOptions[k])+'">'+esc(jobOptions[k])+'</option>';
    }
    render();
    renderPager();
  }catch(e){document.getElementById('resumeList').innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>';}
}

function renderPager(){
  var totalPages=Math.ceil(totalCount/perPage);
  var html='<span>共 '+totalCount+' 份</span>';
  html+='<button '+(currentPage<=1?'disabled':'')+' onclick="loadData('+(currentPage-1)+')">上一页</button>';
  var start=Math.max(1,currentPage-2),end=Math.min(totalPages,currentPage+2);
  if(start>1){html+='<button onclick="loadData(1)">1</button>';if(start>2)html+='<span>…</span>';}
  for(var i=start;i<=end;i++)html+='<button '+(i===currentPage?'class="active"':'')+' onclick="loadData('+i+')">'+i+'</button>';
  if(end<totalPages){if(end<totalPages-1)html+='<span>…</span>';html+='<button onclick="loadData('+totalPages+')">'+totalPages+'</button>';}
  html+='<button '+(currentPage>=totalPages?'disabled':'')+' onclick="loadData('+(currentPage+1)+')">下一页</button>';
  document.getElementById('pager').innerHTML='<div class="pagination">'+html+'</div>';
}

function getTierBadge(r){
  if(!r.total_score&&r.total_score!==0)return '<span class="tier-badge tier-unscored">未打分</span>';
  if(r.eliminated==1||r.tier==='已淘汰')return '<span class="tier-badge tier-eliminated">已淘汰</span>';
  if(r.tier==='推荐面试')return '<span class="tier-badge tier-recommend">推荐面试</span>';
  if(r.tier==='可备选')return '<span class="tier-badge tier-backup">可备选</span>';
  return '<span class="tier-badge tier-reject">暂不考虑</span>';
}

function getScoreBar(score){
  if(score===null||score===undefined)return '';
  var cls=score>=75?'score-high':score>=55?'score-mid':'score-low';
  return '<span class="score-bar"><span class="score-fill '+cls+'" style="width:'+score+'%"></span></span>';
}

function render(){
  var q=(document.getElementById('searchInput').value||'').trim().toLowerCase();
  var jf=document.getElementById('jobFilter').value;
  var tf=document.getElementById('tierFilter').value;
  var sb=document.getElementById('sortBy').value;
  var filtered=[];
  for(var i=0;i<allResumes.length;i++){
    var r=allResumes[i];
    if(q){
      var hay=(r.name+' '+r.secondText+' '+(r.education||'')+' '+(r.applyStatus||'')+' '+(r.city||'')).toLowerCase();
      if(hay.indexOf(q)<0)continue;
    }
    if(jf&&r.jobPosition!==jf)continue;
    if(tf==='recommend'&&r.tier!=='推荐面试')continue;
    if(tf==='backup'&&r.tier!=='可备选')continue;
    if(tf==='reject'&&r.tier!=='暂不考虑')continue;
    if(tf==='eliminated'&&r.eliminated!=1&&r.tier!=='已淘汰')continue;
    if(tf==='unscored'&&r.total_score!==null&&r.total_score!==undefined)continue;
    filtered.push(r);
  }
  if(sb==='score')filtered.sort(function(a,b){return (b.total_score||-1)-(a.total_score||-1)});
  else if(sb==='name')filtered.sort(function(a,b){return (a.name||'').localeCompare(b.name||'')});
  else if(sb==='edu')filtered.sort(function(a,b){var m={'博士':4,'硕士':3,'本科':2,'大专':1};return (m[b.education]||0)-(m[a.education]||0)});

  var scored=0,unscored=0;
  for(var i=0;i<filtered.length;i++){if(filtered[i].total_score!==null&&filtered[i].total_score!==undefined)scored++;else unscored++;}
  document.getElementById('infoBar').textContent='共 '+filtered.length+' 份(已打分 '+scored+',未打分 '+unscored+')';

  var html='';
  if(!filtered.length){document.getElementById('resumeList').innerHTML='<div class="empty-state">无匹配简历</div>';return;}
  html+='<table class="rtable"><thead><tr>';
  html+='<th style="width:30px;">#</th><th>姓名</th><th>性别</th><th>学历</th><th>学校/专业</th><th>岗位</th><th>到岗</th><th>分数</th><th>分档</th>';
  html+='</tr></thead><tbody>';
  for(var i=0;i<filtered.length;i++){
    var r=filtered[i];
    var elimClass=(r.eliminated==1||r.tier==='已淘汰')?' style="color:#999;text-decoration:line-through;"':'';
    html+='<tr onclick="location.href=\'/resume/'+r.id+'\'">';
    html+='<td style="color:#999;">'+r.id+'</td>';
    html+='<td style="font-weight:600;"'+elimClass+'>'+esc(r.name||'')+'</td>';
    html+='<td'+elimClass+'>'+esc(r.gender||'—')+'</td>';
    html+='<td'+elimClass+'>'+esc(r.education||'')+'</td>';
    html+='<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"'+elimClass+'>'+esc((r.secondText||'').replace('毕业于 ',''))+'</td>';
    html+='<td style="font-size:11px;color:#888;"'+elimClass+'>'+esc((r.jobPosition||'').substring(0,10))+'</td>';
    html+='<td style="font-size:11px;"'+elimClass+'>'+esc(r.applyStatus||'')+'</td>';
    if(r.eliminated==1||r.tier==='已淘汰'){
      html+='<td'+elimClass+'>0</td>';
      html+='<td><span class="tier-badge tier-eliminated" title="'+esc(r.eliminated_reason||'')+'">已淘汰</span></td>';
    }else{
      html+='<td>'+(r.total_score!==null&&r.total_score!==undefined?'<b>'+r.total_score+'</b>'+getScoreBar(r.total_score):'—')+'</td>';
      html+='<td>'+getTierBadge(r)+'</td>';
    }
    html+='</tr>';
  }
  html+='</tbody></table>';
  document.getElementById('resumeList').innerHTML=html;
}

async function runScoring(){
  document.getElementById('scoreModal').classList.add('show');
  document.getElementById('scoreStatus').textContent='正在对未打分的简历进行打分...';
  try{
    var r=await fetch('/api/resumes/score',{method:'POST'});
    var j=await r.json();
    document.getElementById('scoreModal').classList.remove('show');
    if(!j.ok){alert('打分失败: '+(j.error||''));return;}
    alert('✅ 打分完成!'+j.data.scored+' 份简历已评分。');
    loadData(1);
  }catch(e){
    document.getElementById('scoreModal').classList.remove('show');
    alert('错误: '+e.message);
  }
}

function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
loadData(1);
</script>
{% endblock %}