Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e233c294b0 | ||
|
|
6d30b8b891 | ||
|
|
666f35931c |
@@ -92,6 +92,30 @@ def auth_logout():
|
|||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/auth/change-password", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def auth_change_password():
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
old_password = data.get("old_password", "")
|
||||||
|
new_password = data.get("new_password", "")
|
||||||
|
if not old_password or not new_password:
|
||||||
|
return jsonify({"error": "旧密码和新密码不能为空"}), 400
|
||||||
|
if len(new_password) < 6:
|
||||||
|
return jsonify({"error": "新密码至少需要6位"}), 400
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
conn = db()
|
||||||
|
try:
|
||||||
|
user = one(conn, "SELECT * FROM users WHERE id=?", (user_id,))
|
||||||
|
if not user or not check_password_hash(user["password_hash"], old_password):
|
||||||
|
return jsonify({"error": "旧密码错误"}), 401
|
||||||
|
new_hash = generate_password_hash(new_password)
|
||||||
|
_exec(conn, "UPDATE users SET password_hash=? WHERE id=?", (new_hash, user_id))
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/auth/me")
|
@bp.route("/api/auth/me")
|
||||||
def auth_me():
|
def auth_me():
|
||||||
if "user_id" not in session:
|
if "user_id" not in session:
|
||||||
|
|||||||
@@ -166,3 +166,89 @@ window.deleteUser = async (uid, username) => {
|
|||||||
toast("删除失败:" + e.message, "error");
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -168,17 +168,9 @@ function renderHome() {
|
|||||||
};
|
};
|
||||||
return buildDeptTable('部门经营情况', d.slice(0, 3)) + buildDeptTable('部门现金流', d.slice(3, 6));
|
return buildDeptTable('部门经营情况', d.slice(0, 3)) + buildDeptTable('部门现金流', d.slice(3, 6));
|
||||||
})()}
|
})()}
|
||||||
${card(`<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`, "p-4")}
|
|
||||||
<div class="grid grid-cols-4 gap-5">
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartSign"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartRev"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartCash"></canvas></div>`, "p-4")}
|
|
||||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartProfit"></canvas></div>`, "p-4")}
|
|
||||||
</div>
|
|
||||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
|
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
renderCharts(financeMonthly);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function chartOptions(yCallback) {
|
function chartOptions(yCallback) {
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ window.toggleUserMenu = (user) => {
|
|||||||
${user.role === 'admin' ? `<button class="w-full text-left px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 transition-colors flex items-center gap-2" onclick="closeUserMenu();openAdminUsers()">
|
${user.role === 'admin' ? `<button class="w-full text-left px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 transition-colors flex items-center gap-2" onclick="closeUserMenu();openAdminUsers()">
|
||||||
<i data-lucide="users" style="width:14px;height:14px"></i>账号管理
|
<i data-lucide="users" style="width:14px;height:14px"></i>账号管理
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
|
<button class="w-full text-left px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 transition-colors flex items-center gap-2" onclick="closeUserMenu();openChangePwdModal()">
|
||||||
|
<i data-lucide="lock" style="width:14px;height:14px"></i>修改密码
|
||||||
|
</button>
|
||||||
<button class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2" onclick="doLogout()">
|
<button class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2" onclick="doLogout()">
|
||||||
<i data-lucide="log-out" style="width:14px;height:14px"></i>退出登录
|
<i data-lucide="log-out" style="width:14px;height:14px"></i>退出登录
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|||||||
@@ -45,9 +45,9 @@
|
|||||||
<div class="w-8 h-px bg-slate-700 my-2"></div>
|
<div class="w-8 h-px bg-slate-700 my-2"></div>
|
||||||
<!-- 导航 Tab 图标 -->
|
<!-- 导航 Tab 图标 -->
|
||||||
<div class="flex flex-col items-center gap-1 w-14" id="sidebarTabs">
|
<div class="flex flex-col items-center gap-1 w-14" id="sidebarTabs">
|
||||||
<div class="sidebar-tab active" data-tab="home" onclick="switchTab('home')" title="首页">
|
<div class="sidebar-tab active" data-tab="home" onclick="switchTab('home')" title="部门">
|
||||||
<i data-lucide="home" style="width:20px;height:20px"></i>
|
<i data-lucide="trending-up" style="width:20px;height:20px"></i>
|
||||||
<span class="text-[10px] mt-1">首页</span>
|
<span class="text-[10px] mt-1">部门</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="业务">
|
<div class="sidebar-tab" data-tab="finance" onclick="switchTab('finance')" title="业务">
|
||||||
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
|
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||||
<div class="flex items-center gap-3 w-full">
|
<div class="flex items-center gap-3 w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.13</span></p>
|
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.16</span></p>
|
||||||
<div class="flex items-center gap-4 mt-1">
|
<div class="flex items-center gap-4 mt-1">
|
||||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user