列表增强: 快捷标签页(全部/今日/有效人员/推荐/待定/已打招呼), 当天数据高亮+今徽章, 日期按北京时间显示, 导出同步支持
This commit is contained in:
+35
-6
@@ -60,6 +60,14 @@ tr:hover td{background:#f8fafc;cursor:pointer}
|
|||||||
.dots{text-align:center;margin:2px 0 14px}
|
.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{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}
|
.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}
|
#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{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}
|
#detailModal.hidden{display:none}
|
||||||
@@ -103,11 +111,19 @@ tr:hover td{background:#f8fafc;cursor:pointer}
|
|||||||
<section class="pane">
|
<section class="pane">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>候选人</h3>
|
<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">
|
<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_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>
|
<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="姓名/学校/备注">
|
<input type="text" id="q_kw" placeholder="姓名/学校/备注">
|
||||||
<button class="btn" onclick="loadList(1)">查询</button>
|
<button class="btn" onclick="loadList(1)">查询</button>
|
||||||
<button class="btn sub" onclick="exportCsv()">导出CSV</button>
|
<button class="btn sub" onclick="exportCsv()">导出CSV</button>
|
||||||
@@ -258,19 +274,32 @@ function renderCharts(s){
|
|||||||
{name:'已打招呼',value:s.greetedTotal,itemStyle:{color:'#16a34a'}}]}]});
|
{name:'已打招呼',value:s.greetedTotal,itemStyle:{color:'#16a34a'}}]}]});
|
||||||
}
|
}
|
||||||
window.addEventListener('resize',()=>charts.forEach(c=>c.resize()));
|
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){
|
async function loadList(p){
|
||||||
pageCur=p||pageCur;
|
pageCur=p||pageCur;syncTabs();
|
||||||
const d=await api(`/api/candidates${qs()}&page=${pageCur}&size=20`);
|
const d=await api(`/api/candidates${qs()}&page=${pageCur}&size=20`);
|
||||||
pageTotal=Math.max(1,Math.ceil(d.total/d.size));
|
pageTotal=Math.max(1,Math.ceil(d.total/d.size));
|
||||||
$('totalInfo').textContent=`共 ${d.total} 人`;
|
$('totalInfo').textContent=`共 ${d.total} 人`;
|
||||||
$('pageNo').textContent=pageCur+' / '+pageTotal;
|
$('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><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>${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><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 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 page(d){const n=pageCur+d;if(n>=1&&n<=pageTotal)loadList(n)}
|
||||||
function exportCsv(){location.href='/api/export.csv'+qs()}
|
function exportCsv(){location.href='/api/export.csv'+qs()}
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|||||||
const kwOk = await p.evaluate(() => document.querySelectorAll('#tbody tr').length >= 0);
|
const kwOk = await p.evaluate(() => document.querySelectorAll('#tbody tr').length >= 0);
|
||||||
ok('关键词筛选不报错', kwOk);
|
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. 候选人详情弹窗 ==');
|
console.log('== 5. 候选人详情弹窗 ==');
|
||||||
await p.evaluate(() => document.querySelector('#tbody tr')?.click()); await sleep(800);
|
await p.evaluate(() => document.querySelector('#tbody tr')?.click()); await sleep(800);
|
||||||
ok('弹窗打开且含简历全文', (await $('#detailBody').innerText()).includes('简历全文'));
|
ok('弹窗打开且含简历全文', (await $('#detailBody').innerText()).includes('简历全文'));
|
||||||
|
|||||||
@@ -166,11 +166,16 @@ app.get('/api/stats', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/candidates', 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 = [];
|
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 (greeted === '1' || greeted === '0') { where.push('greeted = ?'); params.push(greeted); }
|
||||||
if (account) { where.push('account = ?'); params.push(account); }
|
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_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 (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); }
|
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 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 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,
|
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);
|
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 });
|
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) => {
|
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 = [];
|
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 (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_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 (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); }
|
if (q) { where.push('(name LIKE ? OR school LIKE ? OR notes LIKE ?)'); const like = `%${q}%`; params.push(like, like, like); }
|
||||||
|
|||||||
Reference in New Issue
Block a user