Files
opc-manager/static/modules/admin.js
mac 9b6257ff19 v1.1.0-beta: 安全/性能/架构优化 + 账号管理后台 + 视图切换
## 安全与性能
- .env 环境变量、debug=False、except 改 mysql.connector.Error+logging
- attach_common 批量 IN 查询消除 N+1
- 批量 esc() XSS 转义

## 架构
- app.js 拆分为 7 模块 + admin.js
- .form-ctrl 统一表单控件

## 经营管理
- 字段改名:客户名称→项目名称、销售人员→商务负责人
- 必填:项目名称/商务负责人/经营负责人/签约月份/签约金额>0
- 视图切换:确收/毛利 ↔ 回款/费用

## 重点工作与台账
- 统计卡片样式与经营管理统一
- 任务状态简化 3 态
- 优先级点击切换、右键菜单(重命名/副本)
- 修复新建任务绑定错误项目 bug

## 用户体系
- 新增工作台:MCN·无界、无界·无界
- 新增账号:mcn/wuji
- 账号管理后台(admin 限定)
- sidebar 顶部头像+显示名,点击弹菜单
- sidebar sticky 定位

## 其他
- 登录页样式优化(参考 UOC 平台)
- 首页财务趋势拆 3 图
- 业务方案标准资料库双 Tab
2026-06-23 15:54:03 +08:00

169 lines
8.1 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-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");
}
};