列表增强: 快捷标签页(全部/今日/有效人员/推荐/待定/已打招呼), 当天数据高亮+今徽章, 日期按北京时间显示, 导出同步支持

This commit is contained in:
mzl92
2026-08-27 15:45:26 +08:00
parent 3c5f5877bb
commit 194ce81f27
3 changed files with 64 additions and 11 deletions
+35 -6
View File
@@ -60,6 +60,14 @@ tr:hover td{background:#f8fafc;cursor:pointer}
.dots{text-align:center;margin:2px 0 14px}
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#cbd5e1;margin:0 5px;cursor:pointer;transition:all .2s}
.dot.on{background:var(--pri);width:22px;border-radius:5px}
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
.tab{padding:5px 16px;border-radius:18px;border:1px solid var(--line);background:#fff;cursor:pointer;font-size:13px}
.tab:hover{border-color:var(--pri);color:var(--pri)}
.tab.on{background:var(--pri);color:#fff;border-color:var(--pri)}
.tab.on[data-q="valid"]{background:var(--ok);border-color:var(--ok)}
.tab.on[data-q="today"]{background:var(--warn);border-color:var(--warn)}
tr.today td{background:#fef9c3!important}
tr.today:hover td{background:#fde68a!important}
#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}
@@ -103,11 +111,19 @@ tr:hover td{background:#f8fafc;cursor:pointer}
<section class="pane">
<div class="panel">
<h3>候选人</h3>
<div class="tabs" id="quickTabs">
<button class="tab on" data-q="" onclick="quickTab('')">全部</button>
<button class="tab" data-q="today" onclick="quickTab('today')">☀️ 今日</button>
<button class="tab" data-q="valid" onclick="quickTab('valid')">✅ 有效人员</button>
<button class="tab" data-q="rec" onclick="quickTab('rec')">推荐</button>
<button class="tab" data-q="maybe" onclick="quickTab('maybe')">待定</button>
<button class="tab" data-q="greeted" onclick="quickTab('greeted')">已打招呼</button>
</div>
<div class="filters">
<select id="q_level"><option value="">全部级别</option><option>推荐</option><option>待定</option><option>不推荐</option><option>不相关</option></select>
<select id="q_level"><option value="">全部级别</option><option value="有效">有效(推荐+待定)</option><option>推荐</option><option>待定</option><option>不推荐</option><option>不相关</option></select>
<select id="q_greeted"><option value="">打招呼不限</option><option value="1">已打招呼</option><option value="0">未打招呼</option></select>
<select id="q_account"><option value="">全部账号</option><option>hr1</option><option>hr2</option></select>
<input type="date" id="q_from"><span style="color:var(--sub)">至</span><input type="date" id="q_to">
<input type="date" id="q_from" onchange="qToday=0"><span style="color:var(--sub)">至</span><input type="date" id="q_to" onchange="qToday=0">
<input type="text" id="q_kw" placeholder="姓名/学校/备注">
<button class="btn" onclick="loadList(1)">查询</button>
<button class="btn sub" onclick="exportCsv()">导出CSV</button>
@@ -258,19 +274,32 @@ function renderCharts(s){
{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())}`}
let qToday=0;
function quickTab(which){
qToday=which==='today'?1:0;
$('q_level').value=which==='valid'?'有效':(which==='rec'?'推荐':(which==='maybe'?'待定':''));
$('q_greeted').value=which==='greeted'?'1':'';
if(which!==''){$('q_from').value='';$('q_to').value=''}
loadList(1);
}
function syncTabs(){
const which=qToday?'today':($('q_level').value==='有效'?'valid':$('q_level').value==='推荐'?'rec':$('q_level').value==='待定'?'maybe':($('q_greeted').value==='1'?'greeted':''));
document.querySelectorAll('#quickTabs .tab').forEach(b=>b.classList.toggle('on',b.dataset.q===which));
}
function qs(){return `?level=${$('q_level').value}&greeted=${$('q_greeted').value}&account=${$('q_account').value}&today=${qToday}&date_from=${$('q_from').value}&date_to=${$('q_to').value}&q=${encodeURIComponent($('q_kw').value.trim())}`}
async function loadList(p){
pageCur=p||pageCur;
pageCur=p||pageCur;syncTabs();
const d=await api(`/api/candidates${qs()}&page=${pageCur}&size=20`);
pageTotal=Math.max(1,Math.ceil(d.total/d.size));
$('totalInfo').textContent=`共 ${d.total} 人`;
$('pageNo').textContent=pageCur+' / '+pageTotal;
$('tbody').innerHTML=d.rows.map(r=>`<tr onclick="showDetail('${r.id}')">
$('tbody').innerHTML=d.rows.map(r=>`<tr class="${r.is_today?'today':''}" onclick="showDetail('${r.id}')">
<td><b>${esc(r.name)}</b></td><td>${r.age??'-'}</td><td>${esc(r.education)||'-'}</td><td>${esc(r.experience)||'-'}</td>
<td>${esc(r.school)||'-'}</td><td>${esc(r.expected_city)||'-'}</td>
<td><b>${r.score??'-'}</b></td><td><span class="tag ${tagCls[r.score_level]||'no'}">${esc(r.score_level)||'-'}</span></td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis" title="${esc(r.notes)}">${esc(r.notes)||'-'}</td>
<td>${r.greeted?'<span class="tag yes">是</span>':'否'}</td><td>${esc(r.account)||'-'}</td><td>${r.scraped_at}</td></tr>`).join('');
<td>${r.greeted?'<span class="tag yes">是</span>':'否'}</td><td>${esc(r.account)||'-'}</td>
<td>${r.scraped_at}${r.is_today?' <span class="tag maybe">今</span>':''}</td></tr>`).join('');
}
function page(d){const n=pageCur+d;if(n>=1&&n<=pageTotal)loadList(n)}
function exportCsv(){location.href='/api/export.csv'+qs()}
+11
View File
@@ -61,6 +61,17 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const kwOk = await p.evaluate(() => document.querySelectorAll('#tbody tr').length >= 0);
ok('关键词筛选不报错', kwOk);
console.log('== 4b. 快捷标签页 ==');
await p.evaluate(() => quickTab('valid')); await sleep(700);
const validOk = await p.evaluate(() => {
const rows = [...document.querySelectorAll('#tbody tr')];
return rows.length > 0 && rows.every(tr => tr.textContent.includes('推荐') || tr.textContent.includes('待定'));
});
ok('"有效人员"页全部为推荐/待定', validOk);
const todayRows = await p.evaluate(async () => { quickTab('today'); await new Promise(r => setTimeout(r, 600)); return document.querySelectorAll('#tbody tr.today').length; });
ok(`"今日"页当天高亮行(${todayRows})`, todayRows >= 0);
await p.evaluate(() => quickTab('')); await sleep(500);
console.log('== 5. 候选人详情弹窗 ==');
await p.evaluate(() => document.querySelector('#tbody tr')?.click()); await sleep(800);
ok('弹窗打开且含简历全文', (await $('#detailBody').innerText()).includes('简历全文'));
+18 -5
View File
@@ -166,11 +166,16 @@ app.get('/api/stats', requireAuth, (req, res) => {
});
app.get('/api/candidates', requireAuth, (req, res) => {
const { level, greeted, account, date_from, date_to, q } = req.query;
const { level, greeted, account, date_from, date_to, q, today } = req.query;
const where = []; const params = [];
if (level) { where.push('score_level = ?'); params.push(level); }
if (level === '有效') { where.push("score_level IN ('推荐','待定')"); }
else if (level) { where.push('score_level = ?'); params.push(level); }
if (greeted === '1' || greeted === '0') { where.push('greeted = ?'); params.push(greeted); }
if (account) { where.push('account = ?'); params.push(account); }
if (today === '1') { // 北京时间的"今天"(库里是UTC时间戳)
const bjToday = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10);
where.push('scraped_at >= ?'); params.push(new Date(bjToday + 'T00:00:00+08:00').toISOString());
}
if (date_from) { where.push('scraped_at >= ?'); params.push(date_from + 'T00:00'); }
if (date_to) { where.push('scraped_at <= ?'); params.push(date_to + 'T23:59'); }
if (q) { where.push('(name LIKE ? OR school LIKE ? OR notes LIKE ?)'); const like = `%${q}%`; params.push(like, like, like); }
@@ -179,8 +184,11 @@ app.get('/api/candidates', requireAuth, (req, res) => {
const size = Math.min(100, Math.max(10, parseInt(req.query.size) || 20));
const total = rdb.prepare(`SELECT COUNT(*) c FROM candidates ${w}`).get(...params).c;
const rows = rdb.prepare(`SELECT id, name, age, education, experience, school, expected_city, expected_salary,
score, score_level, role_tag, greeted, account, substr(scraped_at,1,10) scraped_at, notes
score, score_level, role_tag, greeted, account, scraped_at, notes
FROM candidates ${w} ORDER BY scraped_at DESC, rowid DESC LIMIT ? OFFSET ?`).all(...params, size, (page - 1) * size);
const bjToday = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10);
const toBjDay = (s) => s ? new Date(new Date(s).getTime() + 8 * 3600e3).toISOString().slice(0, 10) : '';
for (const r of rows) { r.scraped_at = toBjDay(r.scraped_at); r.is_today = r.scraped_at === bjToday ? 1 : 0; }
res.json({ total, page, size, rows });
});
@@ -194,10 +202,15 @@ app.get('/api/candidates/:id', requireAuth, (req, res) => {
});
app.get('/api/export.csv', requireAuth, (req, res) => {
const { level, greeted, date_from, date_to, q } = req.query;
const { level, greeted, date_from, date_to, q, today } = req.query;
const where = []; const params = [];
if (level) { where.push('score_level = ?'); params.push(level); }
if (level === '有效') { where.push("score_level IN ('推荐','待定')"); }
else if (level) { where.push('score_level = ?'); params.push(level); }
if (greeted === '1' || greeted === '0') { where.push('greeted = ?'); params.push(greeted); }
if (today === '1') {
const bjToday = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10);
where.push('scraped_at >= ?'); params.push(new Date(bjToday + 'T00:00:00+08:00').toISOString());
}
if (date_from) { where.push('scraped_at >= ?'); params.push(date_from + 'T00:00'); }
if (date_to) { where.push('scraped_at <= ?'); params.push(date_to + 'T23:59'); }
if (q) { where.push('(name LIKE ? OR school LIKE ? OR notes LIKE ?)'); const like = `%${q}%`; params.push(like, like, like); }