feat(dnfs): 左上角用户信息面板(默认折叠),新增 /whoami 身份接口

dnfs 新增 POST /whoami:解 Token payload 返回 user/is_admin,无或畸形
Token 返回 login=false(HttpOnly Cookie 模式下 JS 无法读 Token,身份
必须由后端下发);static 加 /whoami 代理(Cookie 转 Bearer);前端
dnfs.html 左上新增 userpanel,镜像 minimap 样式默认折叠,展开显示
用户名/角色/退出登录(POST /logout 清 Cookie 后跳登录页)。
This commit is contained in:
qing committed 2026-07-30 09:31:28 +08:00
1 parent 1388fcb72b
commit 8c1956cdeb
5 files changed
+131 -6

No files matched your search

+16
View File
@@ -474,6 +474,22 @@ def proxy_update_note():
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
@app.route('/whoami', methods=['POST', 'OPTIONS'])
def proxy_whoami():
if request.method == "OPTIONS": return _cors(make_response("", 204))
try:
data = request.get_json(force=True, silent=True) or {}
h = {"Content-Type": "application/json"}
if "Authorization" in request.headers: h["Authorization"] = request.headers["Authorization"]
elif request.cookies.get("q4_token"): h["Authorization"] = f"Bearer {request.cookies['q4_token']}" # HttpOnly Cookie 模式:Cookie 转 Bearer
req = _urlreq.Request(f"{DNFS_URL}/whoami", data=_json.dumps(data).encode(), headers=h, method="POST")
with _urlreq.urlopen(req, timeout=15) as r:
return _cors(make_response(r.read().decode(), 200, {"Content-Type": "application/json"}))
except Exception as e:
logs.warning("static", "dnfs whoami 代理失败", extra={"error": str(e)})
return _cors(make_response(_json.dumps({"error": str(e)}), 502, {"Content-Type": "application/json"}))
@app.route('/api/proxy_send_code', methods=['POST'])
def proxy_send_code():
from flask import request, jsonify
+33 -6
View File
@@ -365,6 +365,33 @@ input[type="button"]:disabled {
font-weight: 600;
}
/* ===== User Panel (dnfs 左上角用户信息,镜像 minimap 样式) ===== */
.userpanel {
position: fixed;
left: 1rem;
top: 1rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 80vh;
overflow-y: auto;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-md);
background: rgba(253, 246, 238, 0.72);
border: 1px solid var(--border-light);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
transition: background 0.2s, border-color 0.2s;
}
@media (prefers-color-scheme: dark) {
.userpanel {
background: rgba(30, 23, 18, 0.72);
border-color: var(--border);
}
}
/* ===== Minimap (dnfs) ===== */
.minimap {
position: fixed;
@@ -392,8 +419,8 @@ input[type="button"]:disabled {
border-color: var(--border);
}
}
.minimap:empty { display: none; }
.minimap .tag {
.minimap:empty, .userpanel:empty { display: none; }
.minimap .tag, .userpanel .tag {
display: inline-block;
max-width: 140px;
overflow: hidden;
@@ -416,18 +443,18 @@ input[type="button"]:disabled {
border-radius: 4px;
}
.minimap::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.minimap .tag:hover {
.minimap .tag:hover, .userpanel .tag:hover {
color: var(--accent-dark);
background: var(--accent-soft);
border-color: var(--accent);
}
.minimap .tag.back {
.minimap .tag.back, .userpanel .tag.back {
color: var(--accent);
background: var(--accent-soft);
border-color: var(--accent);
margin-bottom: 0.25rem;
}
.minimap .tag.fold-btn {
.minimap .tag.fold-btn, .userpanel .tag.fold-btn {
color: var(--fg-secondary);
background: transparent;
border: 1px solid var(--border);
@@ -438,7 +465,7 @@ input[type="button"]:disabled {
cursor: pointer;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.minimap .tag.fold-btn:hover {
.minimap .tag.fold-btn:hover, .userpanel .tag.fold-btn:hover {
color: var(--accent-dark);
background: var(--accent-soft);
border-color: var(--accent);
+1
View File
@@ -9,6 +9,7 @@
</head>
<body>
<div class="box">
<div id="userpanel" class="userpanel"></div>
<div id="minimap" class="minimap"></div>
<div id="movebar" class="movebar"></div>
<div class="recommend-list" id="recommend-list"></div>
+59
View File
@@ -654,5 +654,64 @@ function loadMap(children_json) {
}
}
/* by qing — 用户信息面板:左上角,默认折叠;展示用户名 / 角色 / 退出登录。
* 身份来自 dnfs /whoami(HttpOnly Cookie 模式下 JS 无法读 Token,由后端解 payload 返回) */
var userpanel = document.getElementById("userpanel");
var userpanel_expand = false;
var current_user = null;
async function loadUserPanel() {
try {
const res = await fetch('/whoami', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: '{}'
});
if (!res.ok) return;
current_user = await res.json();
renderUserPanel();
} catch (e) {
console.error('获取用户信息失败:', e);
}
}
function renderUserPanel() {
if (current_user == null) return;
userpanel.replaceChildren();
let fold = document.createElement("div");
fold.className = "tag fold-btn";
if (userpanel_expand) {
fold.textContent = "v 折叠";
} else {
fold.textContent = "> " + (current_user.login ? current_user.user : "未登录");
}
fold.addEventListener("click", () => {
userpanel_expand = !userpanel_expand;
renderUserPanel();
});
userpanel.appendChild(fold);
if (!userpanel_expand || !current_user.login) return;
let name_tag = document.createElement("div");
name_tag.className = "tag";
name_tag.textContent = current_user.user;
userpanel.appendChild(name_tag);
let role_tag = document.createElement("div");
role_tag.className = "tag";
role_tag.textContent = current_user.is_admin ? "管理员" : "普通用户";
userpanel.appendChild(role_tag);
let logout_tag = document.createElement("div");
logout_tag.className = "tag back";
logout_tag.textContent = "退出登录";
logout_tag.addEventListener("click", async () => {
try { await fetch('/logout', {method: 'POST'}); } catch (e) {}
location.href = '/login.html';
});
userpanel.appendChild(logout_tag);
}
getNode(0).then(load_notes);
getNode(0).then(loadMap)
loadUserPanel();