Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d30b8b891 |
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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('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') 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 => {
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user