## 安全与性能 - .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
169 lines
8.1 KiB
JavaScript
169 lines
8.1 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");
|
||
}
|
||
};
|