Compare commits

..

1 Commits

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

@@ -116,7 +116,7 @@ function renderFinance() {
const finHeaderBase = `<span class="text-sm text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url(&apos;data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E&apos;) no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${state.finView==='overview'?'selected':''}>总视图</option><option value="quarterly" ${state.finView==='quarterly'?'selected':''}>季度视图</option><option value="monthly" ${state.finView==='monthly'?'selected':''}>月度视图</option></select>${toolDateSelect}`;
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button>`;
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab${state.finFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600">总览</button><button onclick="switchFinFilter('已签约')" class="finance-tab${state.finFilter==='已签约'?' active':''}" style="font-size:14px;font-weight:600">已签约 (${pfs.filter(x=>x.status==='已签约').length})</button><button onclick="switchFinFilter('待签约')" class="finance-tab${state.finFilter==='待签约'?' active':''}" style="font-size:14px;font-weight:600">待签约 (${pfs.filter(x=>x.status==='待签约').length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button></div>`;
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab${state.finFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600">总览</button><button onclick="switchFinFilter('all')" class="finance-tab${state.finFilter==='all'?' active':''}" style="font-size:14px;font-weight:600">所有项目 (${pfs.length})</button><button onclick="switchFinFilter('已签约')" class="finance-tab${state.finFilter==='已签约'?' active':''}" style="font-size:14px;font-weight:600">已签约 (${pfs.filter(x=>x.status==='已签约').length})</button><button onclick="switchFinFilter('待签约')" class="finance-tab${state.finFilter==='待签约'?' active':''}" style="font-size:14px;font-weight:600">待签约 (${pfs.filter(x=>x.status==='待签约').length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button></div>`;
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
${finFilterTabs}
@@ -246,6 +246,7 @@ function renderFinance() {
return `<div id="financeExpense"></div>`;
}
const isUnsigned = state.finFilter === '待签约';
const filterPfs = state.finFilter === 'all' ? pfs : pfs.filter(x => x.status === state.finFilter);
const METRIC_CARDS = [
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
@@ -331,7 +332,7 @@ function renderFinance() {
};
if (state.finView === 'monthly') {
const allPfs = pfs.filter(x => x.status === state.finFilter);
const allPfs = filterPfs;
const now = new Date();
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
if (!state.finMonth) state.finMonth = defaultMonth;
@@ -358,7 +359,7 @@ function renderFinance() {
}
if (state.finView === 'quarterly') {
const allPfs = pfs.filter(x => x.status === state.finFilter);
const allPfs = filterPfs;
const qRanges = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]];
const now = new Date();
if (state.finQuarter === undefined) {
@@ -404,7 +405,7 @@ function renderFinance() {
expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; });
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
};
const allPfs = pfs.filter(x => x.status === state.finFilter);
const allPfs = filterPfs;
let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0;
const rows = [];
allPfs.forEach(pf => {

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.15</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">