- server.js 新增 POST /api/admin/delete:移除本地账号并逐平台 force_logout - admin.html 员工行新增删除按钮(带确认);导入区新增选择文件导入,自动去 BOM
194 lines
8.3 KiB
HTML
194 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>员工账号管理</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; background: #f3f5f7; color: #1f2329; padding: 24px; }
|
|
.wrap { max-width: 920px; margin: 0 auto; }
|
|
h1 { font-size: 20px; margin-bottom: 16px; }
|
|
.card { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
|
.card h2 { font-size: 15px; margin-bottom: 12px; }
|
|
input, textarea, button { font: inherit; }
|
|
input, textarea { border: 1px solid #d0d5dd; border-radius: 6px; padding: 8px 10px; outline: none; }
|
|
input:focus, textarea:focus { border-color: #1677ff; }
|
|
button { border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; background: #1677ff; color: #fff; }
|
|
button.ghost { background: #eef1f5; color: #1f2329; }
|
|
button.danger { background: #f04438; }
|
|
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
.row input { flex: 1; min-width: 140px; }
|
|
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #eef1f5; }
|
|
th { color: #667085; font-weight: 500; }
|
|
.tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
|
.tag.on { background: #e7f6ec; color: #12934a; }
|
|
.tag.off { background: #fdecea; color: #d92d20; }
|
|
td button { padding: 4px 10px; font-size: 13px; margin-right: 6px; }
|
|
textarea { width: 100%; min-height: 110px; font-family: inherit; resize: vertical; }
|
|
.msg { margin-top: 10px; font-size: 13px; white-space: pre-wrap; max-height: 200px; overflow: auto; }
|
|
.msg.err { color: #d92d20; }
|
|
.msg.okm { color: #12934a; }
|
|
.hint { font-size: 12px; color: #98a2b3; margin-top: 6px; }
|
|
#loginBar { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
#loginBar input { flex: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<h1>员工账号管理</h1>
|
|
|
|
<div class="card">
|
|
<div id="loginBar">
|
|
<input id="token" type="password" placeholder="请输入管理口令">
|
|
<button onclick="saveToken()">确定</button>
|
|
</div>
|
|
<div id="msg" class="msg"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>批量导入员工名单</h2>
|
|
<textarea id="csv" placeholder="每行一条:工号,姓名,初始密码 例如: 10001,张三,abc123456 10002,李四,abc123456"></textarea>
|
|
<div class="hint">支持逗号或 Tab 分隔(Excel 复制出来直接粘贴即可);重复工号会覆盖原账号(姓名与密码重置、状态恢复为启用)。</div>
|
|
<div class="row" style="margin-top:10px">
|
|
<button onclick="doImport()">导入</button>
|
|
<button class="ghost" onclick="$('fileInput').click()">选择文件导入</button>
|
|
<button class="ghost" onclick="downloadTemplate()">下载导入模板</button>
|
|
<input id="fileInput" type="file" accept=".csv,.txt" style="display:none" onchange="importFile(this)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>添加员工</h2>
|
|
<div class="row">
|
|
<input id="addNo" placeholder="工号">
|
|
<input id="addName" placeholder="姓名">
|
|
<input id="addPwd" placeholder="初始密码(至少6位)">
|
|
<button onclick="addEmp()">添加</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>员工列表 <span id="total" style="color:#98a2b3;font-weight:400"></span></h2>
|
|
<div class="row" style="margin-bottom:10px"><button class="ghost" onclick="loadList()">刷新</button></div>
|
|
<table>
|
|
<thead><tr><th>工号</th><th>姓名</th><th>状态</th><th>更新时间</th><th>操作</th></tr></thead>
|
|
<tbody id="tbody"><tr><td colspan="5" style="color:#98a2b3">尚未加载</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
$('token').value = localStorage.getItem('adminToken') || '';
|
|
|
|
function saveToken() { localStorage.setItem('adminToken', $('token').value.trim()); show('口令已保存', true); loadList(); }
|
|
function show(text, ok) { const m = $('msg'); m.textContent = text; m.className = 'msg ' + (ok ? 'okm' : 'err'); }
|
|
|
|
async function api(path, body) {
|
|
const res = await fetch(path, {
|
|
method: body ? 'POST' : 'GET',
|
|
headers: { 'Content-Type': 'application/json', 'admin-token': $('token').value.trim() },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const j = await res.json();
|
|
if (j.code !== 0) throw new Error(j.msg || '请求失败');
|
|
return j.data;
|
|
}
|
|
|
|
async function loadList() {
|
|
try {
|
|
const d = await api('/api/admin/employees');
|
|
$('total').textContent = '(共 ' + d.total + ' 人)';
|
|
$('tbody').innerHTML = d.employees.map(e => `
|
|
<tr>
|
|
<td>${e.staffNo}</td><td>${e.name}</td>
|
|
<td><span class="tag ${e.status === 'active' ? 'on' : 'off'}">${e.status === 'active' ? '启用' : '已停用'}</span></td>
|
|
<td>${(e.updatedAt || '').replace('T', ' ').slice(0, 19)}</td>
|
|
<td>
|
|
${e.status === 'active'
|
|
? `<button class="danger" onclick="toggle('${e.staffNo}', false)">停用</button>`
|
|
: `<button class="ghost" onclick="toggle('${e.staffNo}', true)">启用</button>`}
|
|
<button class="ghost" onclick="resetPwd('${e.staffNo}')">重置密码</button>
|
|
<button class="danger" onclick="deleteEmp('${e.staffNo}')">删除</button>
|
|
</td>
|
|
</tr>`).join('') || '<tr><td colspan="5" style="color:#98a2b3">暂无员工,请先导入</td></tr>';
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
async function toggle(staffNo, enable) {
|
|
try {
|
|
await api('/api/admin/' + (enable ? 'enable' : 'disable'), { staffNo });
|
|
show((enable ? '已启用 ' : '已停用 ') + staffNo, true);
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
async function resetPwd(staffNo) {
|
|
const pwd = prompt('为 ' + staffNo + ' 设置新密码(至少 6 位):');
|
|
if (!pwd) return;
|
|
try {
|
|
await api('/api/admin/reset_password', { staffNo, password: pwd });
|
|
show('已重置 ' + staffNo + ' 的密码', true);
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
async function addEmp() {
|
|
try {
|
|
await api('/api/admin/employees', { staffNo: $('addNo').value.trim(), name: $('addName').value.trim(), password: $('addPwd').value });
|
|
show('已添加 ' + $('addNo').value.trim(), true);
|
|
$('addNo').value = $('addName').value = $('addPwd').value = '';
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
async function doImport() {
|
|
try {
|
|
const d = await api('/api/admin/import', { csv: $('csv').value });
|
|
const bad = d.results.filter(r => !r.ok);
|
|
show('导入完成:成功 ' + d.imported + ' / ' + d.total + (bad.length ? '\n失败明细:\n' + bad.map(b => b.line + ' → ' + b.msg).join('\n') : ''), bad.length === 0);
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
// 直接选择模板 CSV 文件导入,不用复制粘贴
|
|
async function importFile(input) {
|
|
const file = input.files[0];
|
|
input.value = '';
|
|
if (!file) return;
|
|
try {
|
|
let text = await file.text();
|
|
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1); // 去 BOM
|
|
const d = await api('/api/admin/import', { csv: text });
|
|
const bad = d.results.filter(r => !r.ok);
|
|
show('文件「' + file.name + '」导入完成:成功 ' + d.imported + ' / ' + d.total + (bad.length ? '\n失败明细:\n' + bad.map(b => b.line + ' → ' + b.msg).join('\n') : ''), bad.length === 0);
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
async function deleteEmp(staffNo) {
|
|
if (!confirm('确定删除 ' + staffNo + ' 吗?删除后该员工立即无法登录,需要重新添加才能恢复。')) return;
|
|
try {
|
|
await api('/api/admin/delete', { staffNo });
|
|
show('已删除 ' + staffNo, true);
|
|
loadList();
|
|
} catch (e) { show(e.message, false); }
|
|
}
|
|
|
|
// 下载 CSV 导入模板(加 BOM 让 Excel 正确识别中文)
|
|
function downloadTemplate() {
|
|
const csv = '\uFEFF工号,姓名,初始密码\n10001,张三,abc123456\n10002,李四,abc123456\n';
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = '员工导入模板.csv';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|