候选人
+
+
+
+
+
+
+
+
-
+
- 至
+ 至
@@ -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=>`
+ $('tbody').innerHTML=d.rows.map(r=>`
| ${esc(r.name)} | ${r.age??'-'} | ${esc(r.education)||'-'} | ${esc(r.experience)||'-'} |
${esc(r.school)||'-'} | ${esc(r.expected_city)||'-'} |
${r.score??'-'} | ${esc(r.score_level)||'-'} |
${esc(r.notes)||'-'} |
- ${r.greeted?'是':'否'} | ${esc(r.account)||'-'} | ${r.scraped_at} |
`).join('');
+ ${r.greeted?'是':'否'} | ${esc(r.account)||'-'} |
+ ${r.scraped_at}${r.is_today?' 今':''} | `).join('');
}
function page(d){const n=pageCur+d;if(n>=1&&n<=pageTotal)loadList(n)}
function exportCsv(){location.href='/api/export.csv'+qs()}
diff --git a/scripts/smoke.js b/scripts/smoke.js
index e1e75c5..d573b04 100644
--- a/scripts/smoke.js
+++ b/scripts/smoke.js
@@ -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('简历全文'));
diff --git a/server.js b/server.js
index bf53440..0a1028a 100644
--- a/server.js
+++ b/server.js
@@ -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); }