Files
tongxunruanjian/account-service/admin.html
T
KIMI 07f018c280 管理页新增:员工删除功能 + 直接选择 CSV 文件导入(不用复制粘贴)
- server.js 新增 POST /api/admin/delete:移除本地账号并逐平台 force_logout
- admin.html 员工行新增删除按钮(带确认);导入区新增选择文件导入,自动去 BOM
2026-08-09 00:54:52 +08:00

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="每行一条:工号,姓名,初始密码&#10;例如:&#10;10001,张三,abc123456&#10;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>