fix(portal): 模态打不开时立刻 resolve,避免 await 永久挂起(表现为点击没反应)

- askConfirm/askText 在 openModal 失败时立即 resolve(false)/resolve(null):
  此前 Promise 永不 resolve,await 会一直挂着,用户看到的就是点了没反应
- toast/openModal 在容器缺失时给出明确提示而不是静默失败
- 顶栏显示页面构建时间(取 portal.py 修改时间),用于一眼确认加载的不是缓存旧页
This commit is contained in:
2026-09-12 16:41:26 +08:00
parent 280c8e5026
commit bbe575f38f
+27 -4
View File
@@ -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"""<!DOCTYPE html>
<span class="role" id="role"></span>
<span class="spacer"></span>
<span class="env" id="env"></span>
<span class="env" id="build" title="页面构建时间:刷新后这个值会变,可确认加载的不是缓存旧页"></span>
<button onclick="doLogout()">退出登录</button>
</header>
<nav id="nav"></nav>
@@ -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 || '<button class="act" onclick="closeModal()">关闭</button>';
$('modal-back').classList.add('on');
back.classList.add('on');
return true;
}
function closeModal() { $('modal-back').classList.remove('on'); }
/** 替代 confirm:返回 Promise<boolean>。 */
function askConfirm(title, text, okLabel) {
return new Promise((resolve) => {
openModal(title, `<p style="font-size:13.5px;line-height:1.8">${esc(text)}</p>`,
const opened = openModal(title, `<p style="font-size:13.5px;line-height:1.8">${esc(text)}</p>`,
`<button class="act" onclick="closeModal();window.__ask(false)">取消</button>
<button class="act primary" onclick="closeModal();window.__ask(true)">${esc(okLabel || '确定')}</button>`);
// 模态打不开时必须**立刻 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) => `<option value="${esc(c)}">${esc(c)}</option>`).join('');
openModal(title,
const opened = openModal(title,
`<label style="font-size:12.5px;color:#5a6b7f">${esc(label)}</label>
<select id="ask-value" style="width:100%;margin-top:6px;padding:7px;border:1px solid #c9d2dd;border-radius:4px;font:inherit">${sel}</select>`,
`<button class="act" onclick="closeModal();window.__ask(null)">取消</button>
<button class="act primary" onclick="closeModal();window.__ask(document.getElementById('ask-value').value)">提交</button>`);
if (!opened) { resolve(null); return; }
window.__ask = resolve;
});
}
return new Promise((resolve) => {
openModal(title,
const opened = openModal(title,
`<label style="font-size:12.5px;color:#5a6b7f">${esc(label)}</label>
<textarea id="ask-value" maxlength="${opts.maxlength || 500}">${esc(opts.value || '')}</textarea>
<div class="muted" style="margin-top:4px">${esc(opts.hint || '')}</div>`,
`<button class="act" onclick="closeModal();window.__ask(null)">取消</button>
<button class="act primary" onclick="closeModal();window.__ask(document.getElementById('ask-value').value)">提交</button>`);
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();
</html>
"""
#: 页面构建标记:取本文件的修改时间。刷新后这个值应当变化 ——
#: 用它一眼确认浏览器加载的不是缓存旧页(旧页缺 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="统一登录门户(按角色分流)")