Files
opc-manager/static/modules/admin.js
mac f619ed34a7 feat: 样式统一 + 月份下拉框扩展
- 品牌色统一为天机阁标准 #4A6CF7,tailwind.config 扩展完整色阶
- styles.css 新增 :root CSS 变量层(brand 色阶、语义色、圆角/阴影 token)
- 全项目 hex/Tailwind 类统一替换为 brand-* CSS 变量(115 处引用)
- border-radius/box-shadow 统一使用 token 变量
- 预留 :root[data-theme="dark"] 暗色主题变量占位
- 所有月份下拉框范围从 3 年/15 月扩展至 2020-2027(8 年 96 月)
2026-07-13 16:34:53 +08:00

255 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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-brand-100 text-brand-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");
}
};