fix(portal): 模态打不开时立刻 resolve,避免 await 永久挂起(表现为点击没反应)
- askConfirm/askText 在 openModal 失败时立即 resolve(false)/resolve(null): 此前 Promise 永不 resolve,await 会一直挂着,用户看到的就是点了没反应 - toast/openModal 在容器缺失时给出明确提示而不是静默失败 - 顶栏显示页面构建时间(取 portal.py 修改时间),用于一眼确认加载的不是缓存旧页
This commit is contained in:
+27
-4
@@ -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="统一登录门户(按角色分流)")
|
||||
|
||||
Reference in New Issue
Block a user