diff --git a/public/index.html b/public/index.html index 8344444..915e5ec 100644 --- a/public/index.html +++ b/public/index.html @@ -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}

候选人

+
+ + + + + + +
- + - 至 + 至 @@ -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); }