Compare commits

...

2 Commits

Author SHA1 Message Date
mac
e233c294b0 revert: 移除业务模块所有项目tab 2026-07-08 11:42:07 +08:00
mac
6d30b8b891 feat: 修改密码+密码可见切换+所有项目tab
- 新增 POST /api/auth/change-password 修改密码接口
- 修改密码弹窗,三次密码输入均带眼睛切换可见
- 业务模块新增「所有项目」tab,展示全部项目
- 版本号 v1.0.0.15
2026-07-08 11:34:00 +08:00
4 changed files with 114 additions and 1 deletions

View File

@@ -92,6 +92,30 @@ def auth_logout():
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")
def auth_me():
if "user_id" not in session:

View File

@@ -166,3 +166,89 @@ window.deleteUser = async (uid, username) => {
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");
}
};

View File

@@ -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()">
<i data-lucide="users" style="width:14px;height:14px"></i>账号管理
</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()">
<i data-lucide="log-out" style="width:14px;height:14px"></i>退出登录
</button>`;

View File

@@ -76,7 +76,7 @@
<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-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.14</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">
<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">