- 新增 POST /api/auth/change-password 修改密码接口 - 修改密码弹窗,三次密码输入均带眼睛切换可见 - 业务模块新增「所有项目」tab,展示全部项目 - 版本号 v1.0.0.15
255 lines
12 KiB
JavaScript
255 lines
12 KiB
JavaScript
// admin.js — 账号管理(仅 admin 可见)
|
||
|
||
window.openAdminUsers = async () => {
|
||
const overlay = document.createElement("div");
|
||
overlay.id = "adminOverlay";
|
||
overlay.className = "fixed inset-0 bg-black/40 z-[9998] flex items-center justify-center p-4";
|
||
overlay.innerHTML = `
|
||
<div class="bg-white rounded-xl shadow-2xl w-full max-w-3xl max-h-[85vh] flex flex-col">
|
||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-200">
|
||
<h2 class="text-lg font-semibold text-slate-800">账号管理</h2>
|
||
<div class="flex items-center gap-2">
|
||
<button class="btn btn-primary btn-sm" onclick="openUserForm()"><i data-lucide="plus" style="width:14px;height:14px"></i>新增账号</button>
|
||
<button class="text-slate-400 hover:text-slate-700" onclick="closeAdminUsers()"><i data-lucide="x"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="overflow-y-auto p-6" id="adminUserList"></div>
|
||
</div>`;
|
||
overlay.addEventListener("click", (e) => { if (e.target === overlay) closeAdminUsers(); });
|
||
document.body.appendChild(overlay);
|
||
if (window.lucide) lucide.createIcons();
|
||
await loadUserList();
|
||
};
|
||
|
||
window.closeAdminUsers = () => {
|
||
const el = document.getElementById("adminOverlay");
|
||
if (el) el.remove();
|
||
};
|
||
|
||
async function loadUserList() {
|
||
const list = document.getElementById("adminUserList");
|
||
if (!list) return;
|
||
list.innerHTML = `<div class="text-center text-slate-400 py-8">加载中...</div>`;
|
||
try {
|
||
const users = await api("/api/users");
|
||
const tenants = await api("/api/tenants");
|
||
list.innerHTML = `
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="border-b border-slate-200 text-slate-500 text-xs">
|
||
<th class="text-left py-2 px-2">用户名</th>
|
||
<th class="text-left py-2 px-2">显示名</th>
|
||
<th class="text-left py-2 px-2">角色</th>
|
||
<th class="text-left py-2 px-2">工作台</th>
|
||
<th class="text-right py-2 px-2">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${users.map(u => `
|
||
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||
<td class="py-3 px-2 font-medium text-slate-800">${esc(u.username)}</td>
|
||
<td class="py-3 px-2 text-slate-700">${esc(u.display_name)}</td>
|
||
<td class="py-3 px-2">
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${u.role === 'admin' ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600'}">${u.role === 'admin' ? '管理员' : 'OPC负责人'}</span>
|
||
</td>
|
||
<td class="py-3 px-2 text-slate-600 text-xs">${(u.tenants || []).map(t => `<span class="inline-block bg-slate-100 rounded px-1.5 py-0.5 mr-1 mb-1">${esc(t)}</span>`).join('') || '<span class="text-slate-400">—</span>'}</td>
|
||
<td class="py-3 px-2 text-right">
|
||
<button class="btn btn-ghost btn-sm" onclick="openUserForm(${u.id})"><i data-lucide="edit-2" style="width:14px;height:14px"></i>编辑</button>
|
||
<button class="btn btn-ghost btn-sm text-red-600 hover:bg-red-50" onclick="deleteUser(${u.id}, '${esc(u.username)}')"><i data-lucide="trash-2" style="width:14px;height:14px"></i>删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
if (window.lucide) lucide.createIcons();
|
||
} catch (e) {
|
||
list.innerHTML = `<div class="text-center text-red-600 py-8">加载失败:${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
window.openUserForm = async (uid) => {
|
||
let user = null;
|
||
let userTenants = [];
|
||
if (uid) {
|
||
try {
|
||
const users = await api("/api/users");
|
||
user = users.find(u => u.id === uid);
|
||
userTenants = user?.tenants || [];
|
||
} catch (e) { toast("加载用户失败", "error"); return; }
|
||
}
|
||
const tenants = await api("/api/tenants");
|
||
|
||
const modal = document.createElement("div");
|
||
modal.id = "userFormModal";
|
||
modal.className = "fixed inset-0 bg-black/40 z-[9999] flex items-center justify-center p-4";
|
||
modal.innerHTML = `
|
||
<div class="bg-white rounded-xl shadow-2xl w-full max-w-md">
|
||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-200">
|
||
<h3 class="text-base font-semibold text-slate-800">${user ? '编辑账号' : '新增账号'}</h3>
|
||
<button class="text-slate-400 hover:text-slate-700" onclick="document.getElementById('userFormModal').remove()"><i data-lucide="x"></i></button>
|
||
</div>
|
||
<form class="p-6 space-y-4" onsubmit="submitUserForm(event, ${uid || 0})">
|
||
<div>
|
||
<label class="block text-xs font-medium text-slate-600 mb-1">用户名 ${user ? '<span class="text-slate-400">(不可修改)</span>' : '<span class="text-red-500">*</span>'}</label>
|
||
<input name="username" class="form-ctrl w-full" value="${user ? esc(user.username) : ''}" ${user ? 'disabled' : 'required'} placeholder="登录用户名">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-slate-600 mb-1">显示名 <span class="text-red-500">*</span></label>
|
||
<input name="display_name" class="form-ctrl w-full" value="${user ? esc(user.display_name) : ''}" required placeholder="如:科普负责人">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-slate-600 mb-1">密码 ${user ? '<span class="text-slate-400">(留空不修改)</span>' : '<span class="text-red-500">*</span>'}</label>
|
||
<input name="password" type="password" class="form-ctrl w-full" ${user ? '' : 'required'} placeholder="${user ? '留空保持原密码' : '登录密码'}">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-slate-600 mb-1">角色</label>
|
||
<select name="role" class="form-ctrl w-full">
|
||
<option value="opc_owner" ${user?.role === 'opc_owner' ? 'selected' : ''}>OPC负责人(仅看分配工作台)</option>
|
||
<option value="admin" ${user?.role === 'admin' ? 'selected' : ''}>管理员(看所有工作台)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-slate-600 mb-1">工作台权限 <span class="text-slate-400">(OPC负责人生效)</span></label>
|
||
<div class="space-y-1.5">
|
||
${tenants.map(t => `
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" name="tenants" value="${esc(t)}" ${userTenants.includes(t) ? 'checked' : ''} class="rounded">
|
||
<span class="text-sm text-slate-700">${esc(t)}</span>
|
||
</label>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end gap-2 pt-2">
|
||
<button type="button" class="btn btn-ghost btn-sm" onclick="document.getElementById('userFormModal').remove()">取消</button>
|
||
<button type="submit" class="btn btn-primary btn-sm">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>`;
|
||
modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });
|
||
document.body.appendChild(modal);
|
||
if (window.lucide) lucide.createIcons();
|
||
};
|
||
|
||
window.submitUserForm = async (event, uid) => {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const fd = new FormData(form);
|
||
const payload = {
|
||
username: fd.get("username"),
|
||
display_name: fd.get("display_name"),
|
||
password: fd.get("password"),
|
||
role: fd.get("role"),
|
||
tenants: fd.getAll("tenants"),
|
||
};
|
||
try {
|
||
if (uid) {
|
||
await api(`/api/users/${uid}`, { method: "PUT", body: JSON.stringify(payload) });
|
||
toast("已更新", "success");
|
||
} else {
|
||
await api("/api/users", { method: "POST", body: JSON.stringify(payload) });
|
||
toast("已新增", "success");
|
||
}
|
||
document.getElementById("userFormModal").remove();
|
||
await loadUserList();
|
||
} catch (e) {
|
||
toast("保存失败:" + e.message, "error");
|
||
}
|
||
};
|
||
|
||
window.deleteUser = async (uid, username) => {
|
||
if (!confirm(`确认删除账号「${username}」?此操作不可撤销。`)) return;
|
||
try {
|
||
await api(`/api/users/${uid}`, { method: "DELETE" });
|
||
toast("已删除", "success");
|
||
await loadUserList();
|
||
} catch (e) {
|
||
toast("删除失败:" + e.message, "error");
|
||
}
|
||
};
|
||
|
||
// 修改密码弹窗
|
||
window.openChangePwdModal = () => {
|
||
const overlay = document.createElement("div");
|
||
overlay.id = "changePwdOverlay";
|
||
overlay.className = "fixed inset-0 bg-black/40 z-[9999] flex items-center justify-center p-4";
|
||
overlay.innerHTML = `
|
||
<div class="bg-white rounded-xl shadow-2xl w-full max-w-sm">
|
||
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-200">
|
||
<h2 class="text-lg font-semibold text-slate-800">修改密码</h2>
|
||
<button class="text-slate-400 hover:text-slate-700" onclick="closeChangePwdModal()"><i data-lucide="x"></i></button>
|
||
</div>
|
||
<form class="p-6 grid gap-4" onsubmit="doChangePwd(event)" novalidate>
|
||
<label class="block">
|
||
<span class="text-sm text-slate-500 mb-1 block">旧密码</span>
|
||
<div class="relative">
|
||
<input type="password" name="old_password" required class="form-ctrl pr-10" placeholder="输入旧密码">
|
||
<button type="button" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 p-1" onclick="togglePwdEye(this)"><i data-lucide="eye-off" style="width:16px;height:16px"></i></button>
|
||
</div>
|
||
</label>
|
||
<label class="block">
|
||
<span class="text-sm text-slate-500 mb-1 block">新密码</span>
|
||
<div class="relative">
|
||
<input type="password" name="new_password" required minlength="6" class="form-ctrl pr-10" placeholder="至少6位">
|
||
<button type="button" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 p-1" onclick="togglePwdEye(this)"><i data-lucide="eye-off" style="width:16px;height:16px"></i></button>
|
||
</div>
|
||
</label>
|
||
<label class="block">
|
||
<span class="text-sm text-slate-500 mb-1 block">确认新密码</span>
|
||
<div class="relative">
|
||
<input type="password" name="confirm_password" required minlength="6" class="form-ctrl pr-10" placeholder="再次输入新密码">
|
||
<button type="button" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 p-1" onclick="togglePwdEye(this)"><i data-lucide="eye-off" style="width:16px;height:16px"></i></button>
|
||
</div>
|
||
</label>
|
||
<div class="flex justify-end gap-3 pt-2">
|
||
<button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeChangePwdModal()">取消</button>
|
||
<button type="submit" class="btn btn-primary btn-sm px-8">确认修改</button>
|
||
</div>
|
||
</form>
|
||
</div>`;
|
||
overlay.addEventListener("click", (e) => { if (e.target === overlay) closeChangePwdModal(); });
|
||
document.body.appendChild(overlay);
|
||
if (window.lucide) lucide.createIcons();
|
||
};
|
||
|
||
window.togglePwdEye = (btn) => {
|
||
const input = btn.parentElement.querySelector("input");
|
||
const icon = btn.querySelector("i");
|
||
if (input.type === "password") {
|
||
input.type = "text";
|
||
icon.setAttribute("data-lucide", "eye");
|
||
} else {
|
||
input.type = "password";
|
||
icon.setAttribute("data-lucide", "eye-off");
|
||
}
|
||
if (window.lucide) lucide.createIcons({ elements: [icon] });
|
||
};
|
||
|
||
window.closeChangePwdModal = () => {
|
||
const el = document.getElementById("changePwdOverlay");
|
||
if (el) el.remove();
|
||
};
|
||
|
||
window.doChangePwd = async (e) => {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const fd = new FormData(form);
|
||
const oldPwd = fd.get("old_password");
|
||
const newPwd = fd.get("new_password");
|
||
const confirmPwd = fd.get("confirm_password");
|
||
if (newPwd !== confirmPwd) { toast("两次输入的新密码不一致", "error"); return; }
|
||
if (newPwd.length < 6) { toast("新密码至少需要6位", "error"); return; }
|
||
try {
|
||
const resp = await fetch("/api/auth/change-password", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ old_password: oldPwd, new_password: newPwd }),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok) { toast(data.error || "修改失败", "error"); return; }
|
||
closeChangePwdModal();
|
||
toast("密码修改成功", "success");
|
||
} catch (err) {
|
||
toast("请求失败:" + err.message, "error");
|
||
}
|
||
};
|