diff --git a/tools/portal.py b/tools/portal.py index fac6d14..4486431 100644 --- a/tools/portal.py +++ b/tools/portal.py @@ -38,6 +38,7 @@ from __future__ import annotations import argparse import sys import uuid +from datetime import datetime from pathlib import Path from typing import Any @@ -552,6 +553,7 @@ PAGE = r""" + @@ -583,8 +585,11 @@ async function jpost(url, body) { 17- 文档明确要求:不使用浏览器原生 alert/prompt 作为正式交互方案, 不能只用控制台日志代替用户提示。这里用 Toast + 模态框替代。 */ +const PAGE_BUILD = "__BUILD__"; + function toast(text, kind) { const box = $('toasts'); + if (!box) { console.error('[portal] #toasts 缺失,退回原生提示'); (kind === 'bad' ? console.error : console.log)(text); return; } const el = document.createElement('div'); el.className = 'toast ' + (kind || ''); el.textContent = text; @@ -598,19 +603,29 @@ function bad(text) { toast(text, 'bad'); } function warn(text) { toast(text, 'warn'); } function openModal(title, bodyHtml, footHtml) { + const back = $('modal-back'); + if (!back) { + // 兜底:容器缺失(多半是浏览器缓存了旧页面)时不要静默失败 + console.error('[portal] #modal-back 缺失:页面可能是旧版本,请硬刷新(Ctrl+F5)'); + window.alert(String(title) + '\n\n页面组件缺失,请硬刷新后再试(Ctrl+F5)'); + return false; + } $('modal-title').textContent = title; $('modal-body').innerHTML = bodyHtml; $('modal-foot').innerHTML = footHtml || ''; - $('modal-back').classList.add('on'); + back.classList.add('on'); + return true; } function closeModal() { $('modal-back').classList.remove('on'); } /** 替代 confirm:返回 Promise。 */ function askConfirm(title, text, okLabel) { return new Promise((resolve) => { - openModal(title, `

${esc(text)}

`, + const opened = openModal(title, `

${esc(text)}

`, ` `); + // 模态打不开时必须**立刻 resolve**,否则 await 永远挂着 —— 表现就是"点了没反应" + if (!opened) { resolve(false); return; } window.__ask = resolve; }); } @@ -621,21 +636,23 @@ function askText(title, label, options) { if (opts.choices) { return new Promise((resolve) => { const sel = opts.choices.map((c) => ``).join(''); - openModal(title, + const opened = openModal(title, ` `, ` `); + if (!opened) { resolve(null); return; } window.__ask = resolve; }); } return new Promise((resolve) => { - openModal(title, + const opened = openModal(title, `
${esc(opts.hint || '')}
`, ` `); + if (!opened) { resolve(null); return; } window.__ask = resolve; }); } @@ -735,6 +752,7 @@ function showApp() { $('who').textContent = ME.username + '(' + ME.user_id + ')'; $('role').textContent = ME.roles.join(' / ') || '无角色'; $('env').textContent = (ME.environment?.mode || '') + ' · ' + (ME.environment?.mysql || ME.environment?.target || ''); + if ($('build')) $('build').textContent = 'build ' + PAGE_BUILD; VIEW = ME.view; const tabs = [{ id: VIEW, label: ME.view_label }]; // 多角色时允许手动切到其他已具备的界面,便于一次演示 @@ -1848,6 +1866,11 @@ boot(); """ +#: 页面构建标记:取本文件的修改时间。刷新后这个值应当变化 —— +#: 用它一眼确认浏览器加载的不是缓存旧页(旧页缺 Toast/模态容器时,点击会静默失效)。 +_PAGE_BUILD = datetime.fromtimestamp(Path(__file__).stat().st_mtime).strftime("%m-%d %H:%M") +PAGE = PAGE.replace("__BUILD__", _PAGE_BUILD) + def main() -> None: parser = argparse.ArgumentParser(description="统一登录门户(按角色分流)")