diff --git a/public/index.html b/public/index.html
index 5625e1d..9ab33c5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -52,12 +52,15 @@ tr:hover td{background:#f8fafc;cursor:pointer}
#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}
+#userModal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:8;display:flex;align-items:center;justify-content:center}
+#userModal.hidden{display:none}
@@ -143,7 +149,7 @@ async function logout(){await api('/api/logout',{method:'POST'});location.reload
async function enter(m){
me=m;$('loginBox').classList.add('hidden');$('main').classList.remove('hidden');$('logoutBtn').classList.remove('hidden');
$('whoami').textContent=`${m.username} · ${m.role==='super'?'超管':'HR'}`;
- if(m.role==='super'){$('userPanel').classList.remove('hidden');loadUsers()}
+ if(m.role==='super'){$('userMgrBtn').classList.remove('hidden')}
loadStats().catch(e=>dataErr(e));loadList(1).catch(e=>dataErr(e));
}
function dataErr(e){
@@ -199,20 +205,29 @@ async function showDetail(id){
简历全文
${esc(r.raw_text)}`;
$('detailModal').classList.remove('hidden');
}
+async function openUserModal(){
+ $('userModal').classList.remove('hidden');
+ loadUsers().catch(()=>toast('用户列表加载失败'));
+}
async function loadUsers(){
const us=await api('/api/users');
$('utable').innerHTML=us.map(u=>`
| ${u.id} | ${esc(u.username)} | ${u.role==='super'?'超管':'HR'} |
- ${u.disabled?'停用':'正常'} |
+ ${u.disabled?'已停用':'正常'} |
${u.created_at||''} | ${u.last_login_at||'-'} |
- ${u.role==='super'?'-':`
+ | ${u.role==='super'?'-':`
`} |
`).join('');
}
async function addUser(){
try{await api('/api/users',{method:'POST',body:JSON.stringify({username:$('nu_user').value,password:$('nu_pass').value})});toast('已创建');$('nu_user').value=$('nu_pass').value='';loadUsers()}
catch(e){toast(e.message)}
}
-async function toggleUser(id){await api(`/api/users/${id}/toggle`,{method:'POST'});loadUsers()}
+async function toggleUser(id,name,disabled){
+ const verb=disabled?'启用':'停用';
+ if(!confirm(`确定${verb}账号「${name}」吗?`))return;
+ try{await api(`/api/users/${id}/toggle`,{method:'POST'});toast('已'+verb);loadUsers()}
+ catch(e){toast(e.message)}
+}
async function resetPwd(id,name){
const p=prompt('为 '+name+' 设置新密码(至少6位)');if(!p)return;
try{await api(`/api/users/${id}/resetpwd`,{method:'POST',body:JSON.stringify({password:p})});toast('已重置')}
diff --git a/scripts/smoke.js b/scripts/smoke.js
index a1d7ea2..d91c2b6 100644
--- a/scripts/smoke.js
+++ b/scripts/smoke.js
@@ -22,12 +22,16 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
ok('详情弹窗隐藏', await p.evaluate(() => getComputedStyle(document.getElementById('detailModal')).display === 'none'));
await p.screenshot({ path: '/tmp/smoke-shots/01-init.png' });
- console.log('== 2. 注册超管并自动登录 ==');
- await $('#f_user').fill('testsuper');
- await $('#f_pass').fill('test123456');
+ console.log('== 2. 登录(新实例注册超管/已有实例用测试账号) ==');
+ const boot = await p.evaluate(async () => (await (await fetch('/api/bootstrap')).json()));
+ const SU = process.env.SMOKE_USER || 'testsuper';
+ const SP = process.env.SMOKE_PASS || 'test123456';
+ await $('#f_user').fill(SU);
+ await $('#f_pass').fill(SP);
await $('#loginBtn').click();
await p.waitForNavigation({ waitUntil: 'networkidle', timeout: 8000 }).catch(() => {});
await sleep(800);
+ ok(boot.needsInit ? '注册超管成功' : '测试账号登录成功', await $('#main').isVisible());
ok('进入主界面', await $('#main').isVisible());
ok('身份=超管', (await $('#whoami').textContent()).includes('超管'));
await p.screenshot({ path: '/tmp/smoke-shots/02-dashboard.png' });
@@ -64,13 +68,17 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
});
ok('CSV表头正确', csv.includes('姓名') && csv.includes('门槛备注'));
- console.log('== 7. 超管建HR账号 ==');
- ok('账号管理面板可见', await $('#userPanel').isVisible());
+ console.log('== 7. 顶栏入口+弹框建HR账号 ==');
+ ok('顶栏账号管理入口可见', await $('#userMgrBtn').isVisible());
+ await p.evaluate(() => openUserModal()); await sleep(600);
+ ok('账号管理弹框打开', await $('#userModal').isVisible());
await $('#nu_user').fill('hr01'); await $('#nu_pass').fill('hr123456');
await $('button:has-text("创建HR账号")').click(); await sleep(700);
const uText = await $('#utable').innerText();
ok('用户列表含hr01', uText.includes('hr01'));
+ ok('停用/启用按钮存在', uText.includes('停用'));
await p.screenshot({ path: '/tmp/smoke-shots/04-users.png' });
+ await p.evaluate(() => document.getElementById('userModal').classList.add('hidden'));
console.log('== 8. 退出 → HR登录 ==');
await p.evaluate(() => logout()); await sleep(800);
@@ -78,7 +86,7 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
await $('#f_user').fill('hr01'); await $('#f_pass').fill('hr123456');
await $('#loginBtn').click(); await sleep(900);
ok('HR进入主界面', await $('#main').isVisible());
- ok('HR看不到账号管理', await p.evaluate(() => !document.getElementById('userPanel') || document.getElementById('userPanel').classList.contains('hidden')));
+ ok('HR看不到账号管理入口', await p.evaluate(() => getComputedStyle(document.getElementById('userMgrBtn')).display === 'none'));
const forbidden = await p.evaluate(async () => {
const r = await fetch('/api/users'); return r.status;
});