账号管理改顶栏入口+弹框交互; 停用/启用带确认; 冒烟支持已有实例(SMOKE_USER)

This commit is contained in:
mzl92
2026-08-27 14:21:39 +08:00
parent 520ffb78a1
commit 34efe3842a
2 changed files with 42 additions and 19 deletions
+28 -13
View File
@@ -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}
</style>
</head>
<body>
<div class="topbar">
<b>📋 招聘数据后台</b><span id="whoami" style="color:#94a3b8;font-size:12px"></span>
<span class="sp"></span>
<button id="userMgrBtn" class="hidden" onclick="openUserModal()">账号管理</button>
<button id="logoutBtn" class="hidden" onclick="logout()">退出登录</button>
</div>
<div class="wrap">
@@ -98,16 +101,19 @@ tr:hover td{background:#f8fafc;cursor:pointer}
<button class="btn sub small" onclick="page(1)">下一页</button>
</div>
</div>
<div class="panel hidden" id="userPanel">
<h3>账号管理(仅超管)</h3>
<div class="filters">
<input type="text" id="nu_user" placeholder="新HR用户名" style="width:140px">
<input type="password" id="nu_pass" placeholder="密码至少6位" style="width:140px">
<button class="btn" onclick="addUser()">创建HR账号</button>
</div>
<table><thead><tr><th>ID</th><th>用户名</th><th>角色</th><th>状态</th><th>创建时间</th><th>最近登录</th><th>操作</th></tr></thead>
<tbody id="utable"></tbody></table>
<div class="panel hidden" id="userPanel"></div>
<div id="userModal" onclick="if(event.target===this)this.classList.add('hidden')" class="hidden">
<div class="panel" style="width:780px;max-width:94vw;max-height:88vh;overflow:auto">
<h3>账号管理 <button class="btn sub small" style="float:right" onclick="document.getElementById('userModal').classList.add('hidden')">关闭</button></h3>
<div class="filters">
<input type="text" id="nu_user" placeholder="新HR用户名" style="width:140px">
<input type="password" id="nu_pass" placeholder="密码至少6位" style="width:140px">
<button class="btn" onclick="addUser()">创建HR账号</button>
</div>
<table><thead><tr><th>ID</th><th>用户名</th><th>角色</th><th>状态</th><th>创建时间</th><th>最近登录</th><th>操作</th></tr></thead>
<tbody id="utable"></tbody></table>
<div style="color:var(--sub);font-size:12px;margin-top:8px">提示:停用后该账号无法登录,已登录的会话在下次请求时失效;启用后恢复。</div>
</div>
</div>
</div>
<div id="detailModal" onclick="if(event.target===this)this.classList.add('hidden')" class="hidden">
@@ -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){
<h3 style="font-size:13px">简历全文</h3><pre>${esc(r.raw_text)}</pre>`;
$('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=>`<tr>
<td>${u.id}</td><td><b>${esc(u.username)}</b></td><td>${u.role==='super'?'超管':'HR'}</td>
<td>${u.disabled?'<span class="tag irr">停用</span>':'<span class="tag rec">正常</span>'}</td>
<td>${u.disabled?'<span class="tag irr">已停用</span>':'<span class="tag rec">正常</span>'}</td>
<td>${u.created_at||''}</td><td>${u.last_login_at||'-'}</td>
<td>${u.role==='super'?'-':`<button class="btn sub small" onclick="toggleUser(${u.id})">${u.disabled?'启用':'停用'}</button>
<td>${u.role==='super'?'-':`<button class="btn small" style="${u.disabled?'':'background:var(--warn)'}" onclick="toggleUser(${u.id},'${esc(u.username)}',${u.disabled})">${u.disabled?'启用':'停用'}</button>
<button class="btn sub small" onclick="resetPwd(${u.id},'${esc(u.username)}')">重置密码</button>`}</td></tr>`).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('已重置')}
+14 -6
View File
@@ -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;
});