feat(shell): tabs 多 iframe 常驻保活,切标签不再销毁功能页
单 iframe 换 src 会卸载文档,WebSocket/轮询/传输状态全灭。 改为一标签一懒创建 iframe,切换只改 visibility;F5/⟳ 只 reload 激活帧。
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<!--
|
||||
tabs.html - ESP32 调试控制台「单窗口多标签」外壳
|
||||
顶部一排标签, 下方 iframe 承载各功能页(pages/ 下功能页, lab/ 下实验页)。
|
||||
各页面原样复用, 互不干扰; 页面内的"新窗口打开"/普通链接会在 iframe 内跳转,
|
||||
外壳监听 iframe 的 load 事件同步高亮当前标签。
|
||||
激活标签存 localStorage(dcu_tab), 下次打开自动恢复。
|
||||
顶部一排标签, 下方 **多 iframe 宿主** 承载各功能页(pages/ 下功能页, lab/ 下实验页)。
|
||||
|
||||
保活约定(2026-09): 每个标签一个懒创建 iframe,切换只改可见性,**不改 src、不销毁**。
|
||||
否则功能页内的 WebSocket/轮询/传输状态会随文档卸载全部丢失——「切走再切回连接全灭」。
|
||||
|
||||
激活标签存 localStorage(dcu_tab), 下次打开自动恢复(只预载该标签)。
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
@@ -50,7 +52,20 @@
|
||||
.tabbar .spacer { flex: 1 1 auto; }
|
||||
.tabbar .hint { display: flex; align-items: center; color: var(--text-label);
|
||||
font-size: .72rem; padding: 0 8px; }
|
||||
#view { flex: 1 1 auto; width: 100%; border: none; background: var(--bg-root); display: block; }
|
||||
|
||||
/* 多 iframe 宿主:占满剩余高度;子帧绝对定位铺满。
|
||||
隐藏用 visibility 而非 display:none —— 少触发引擎对不可见文档的定时器/绘制压制,
|
||||
WebSocket 与轮询更稳。 */
|
||||
#viewHost {
|
||||
flex: 1 1 auto; position: relative; width: 100%;
|
||||
background: var(--bg-root); overflow: hidden;
|
||||
}
|
||||
.page-frame {
|
||||
position: absolute; left: 0; top: 0; width: 100%; height: 100%;
|
||||
border: none; background: var(--bg-root);
|
||||
visibility: hidden; pointer-events: none;
|
||||
}
|
||||
.page-frame.active { visibility: visible; pointer-events: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -60,7 +75,8 @@
|
||||
<div class="spacer"></div>
|
||||
<div class="hint">⟳ 刷新当前页 · F5</div>
|
||||
</nav>
|
||||
<iframe id="view" title="功能页" src="about:blank"></iframe>
|
||||
<!-- 外壳标记:注入脚本用 #viewHost 判定「标签外壳」;各功能页是其子 iframe.page-frame -->
|
||||
<div id="viewHost" data-shell="1"></div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
@@ -73,9 +89,12 @@
|
||||
{ page: 'lab/spi_test.html', label: 'SPI/ECAT 通信测速', ico: '⚡', color: 'var(--ble)' }
|
||||
];
|
||||
var bar = document.getElementById('tabbar');
|
||||
var view = document.getElementById('view');
|
||||
var host = document.getElementById('viewHost');
|
||||
var spacer = bar.querySelector('.spacer');
|
||||
var LS = 'dcu_tab';
|
||||
/** page → HTMLIFrameElement;懒创建后常驻,切标签不销毁。 */
|
||||
var frames = Object.create(null);
|
||||
var activePage = null;
|
||||
|
||||
function baseName(p){ return (p || '').replace(/^.*\//, '').split('?')[0]; }
|
||||
|
||||
@@ -88,14 +107,52 @@
|
||||
for (var i=0;i<TABS.length;i++) if (baseName(TABS[i].page) === b) return TABS[i].page;
|
||||
return null; // 未知页(如从页面内跳到别的 html)
|
||||
}
|
||||
|
||||
function setActive(path){
|
||||
var tabs = bar.querySelectorAll('.tab');
|
||||
for (var i=0;i<tabs.length;i++){
|
||||
tabs[i].classList.toggle('active', tabs[i].getAttribute('data-page') === path);
|
||||
}
|
||||
}
|
||||
|
||||
/** 懒创建:首次进入某标签才挂 iframe;已存在则原样返回(文档未卸载)。 */
|
||||
function ensureFrame(page){
|
||||
var f = frames[page];
|
||||
if (f) return f;
|
||||
f = document.createElement('iframe');
|
||||
f.className = 'page-frame';
|
||||
f.setAttribute('data-page', page);
|
||||
f.title = page;
|
||||
f.src = page;
|
||||
host.appendChild(f);
|
||||
frames[page] = f;
|
||||
f.addEventListener('load', function(){
|
||||
/* 非激活帧的延迟 load 不得抢高亮/记忆(懒创建时目标可能已切走) */
|
||||
if (activePage !== page) return;
|
||||
var p = null;
|
||||
try { p = f.contentWindow.location.pathname; } catch(e){ return; }
|
||||
var tab = pageToTab(p);
|
||||
if (tab && tab === page) {
|
||||
setActive(tab);
|
||||
try { localStorage.setItem(LS, tab); } catch(e){}
|
||||
}
|
||||
});
|
||||
return f;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换标签:只改可见性。**绝不改 src / remove**——那会销毁文档,
|
||||
* 把功能页里的 WebSocket、轮询、传输状态一并清掉。
|
||||
*/
|
||||
function go(path, save){
|
||||
view.src = path;
|
||||
var f = ensureFrame(path);
|
||||
for (var p in frames){
|
||||
if (!Object.prototype.hasOwnProperty.call(frames, p)) continue;
|
||||
frames[p].classList.toggle('active', p === path);
|
||||
}
|
||||
/* 兜底:目标帧若因异常不在 frames(理论不会),仍标 active */
|
||||
if (f && !f.classList.contains('active')) f.classList.add('active');
|
||||
activePage = path;
|
||||
setActive(path);
|
||||
if (save !== false) { try { localStorage.setItem(LS, path); } catch(e){} }
|
||||
}
|
||||
@@ -111,15 +168,7 @@
|
||||
bar.insertBefore(b, spacer);
|
||||
});
|
||||
|
||||
// iframe 内导航(页面里的链接/新窗口打开覆写)后, 同步高亮对应标签
|
||||
view.addEventListener('load', function(){
|
||||
var p = null;
|
||||
try { p = view.contentWindow.location.pathname; } catch(e){ return; }
|
||||
var tab = pageToTab(p);
|
||||
if (tab) { setActive(tab); try { localStorage.setItem(LS, tab); } catch(e){} }
|
||||
});
|
||||
|
||||
// 初始标签: 上次记忆, 否则 OTA 主页。
|
||||
// 初始标签: 上次记忆, 否则 OTA 主页。只预载该标签,其余首次点开再创建。
|
||||
// 取 pageToTab 的返回值而非 localStorage 原值, 这样旧版存的裸文件名
|
||||
// (can.html) 会被自动迁移成新的 pages/can.html。
|
||||
var init = 'pages/index.html';
|
||||
|
||||
Reference in New Issue
Block a user