Files
DCU_DesktopController/tabs.html
T
cbb d2c86c425a init: DCU ESP32 调试控制台前端与本地页面服务
纯前端上位机界面 + esp32_server.py 静态服务/桌面窗口外壳, 不含固件、
不直接访问硬件; 业务 API 由 ESP32_COMM 固件 80 端口跨域提供。

- tabs.html 单窗口多标签外壳, iframe 承载各功能页
- index.html OTA 中继(ESP32 WiFi / GD32 relay / BLE), js/ota + js/ble
- can.html / can_monitor.html CAN 通信与接收监控, js/can
- motor.html 电机 CAN FD 控制台, js/motor
- continuity_test.html LAN9252 通断测试, spi_test.html SPI 测速(临时, 自包含)
- 三入口: esp32_app.vbs 桌面窗口 / esp32_web.vbs 浏览器 / serve.bat 控制台调试
- 补 README(页面与目录约定、?v= 缓存版本等改动须知)、.gitignore、
  .gitattributes(* -text 保持 Windows 换行原样)
2026-09-03 21:30:32 +08:00

125 lines
5.1 KiB
HTML

<!--
tabs.html - ESP32 调试控制台「单窗口多标签」外壳
顶部一排标签, 下方 iframe 承载各功能页(index/can/motor/...)。
各页面原样复用, 互不干扰; 页面内的"新窗口打开"/普通链接会在 iframe 内跳转,
外壳监听 iframe 的 load 事件同步高亮当前标签。
激活标签存 localStorage(dcu_tab), 下次打开自动恢复。
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 调试控制台 — DCU</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="css/base.css?v=857f83a5">
<style>
html, body { height: 100%; margin: 0; }
body {
background: var(--bg-root); color: var(--text);
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
display: flex; flex-direction: column; overflow: hidden;
}
.tabbar {
display: flex; align-items: stretch; gap: 2px;
background: var(--bg-panel); border-bottom: 1px solid var(--border);
padding: 0 8px; height: 42px; flex: 0 0 auto;
user-select: none; -webkit-user-select: none;
}
.tabbar .brand {
display: flex; align-items: center; gap: 8px;
padding: 0 12px 0 4px; margin-right: 6px;
border-right: 1px solid var(--border);
font-weight: 600; font-size: .86rem; color: var(--text-dim);
letter-spacing: .02em;
}
.tabbar .brand .dot { width: 9px; height: 9px; border-radius: 50%;
background: var(--esp32); box-shadow: 0 0 8px var(--esp32); }
.tab {
appearance: none; border: none; background: transparent; cursor: pointer;
color: var(--text-dim); font-size: .84rem; font-family: inherit;
padding: 0 16px; display: flex; align-items: center; gap: 7px;
border-bottom: 2px solid transparent; transition: color .12s, background .12s;
white-space: nowrap;
}
.tab:hover { color: var(--text); background: var(--bg-hover); }
.tab.active { color: var(--text); border-bottom-color: var(--esp32); background: var(--bg-card); }
.tab .ico { font-size: .95rem; opacity: .9; }
.tab .accent { width: 7px; height: 7px; border-radius: 50%; opacity: .55; }
.tab.active .accent { opacity: 1; }
.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; }
</style>
</head>
<body>
<nav class="tabbar" id="tabbar">
<div class="brand"><span class="dot"></span>ESP32 调试控制台</div>
<!-- 标签由 TABS 数组渲染 -->
<div class="spacer"></div>
<div class="hint">⟳ 刷新当前页 · F5</div>
</nav>
<iframe id="view" title="功能页" src="about:blank"></iframe>
<script>
(function(){
var TABS = [
{ page: 'index.html', label: 'OTA 中继', ico: '⬆', color: 'var(--esp32)' },
{ page: 'can.html', label: 'CAN 通信', ico: '⇄', color: 'var(--can)' },
{ page: 'can_monitor.html', label: 'CAN 监控', ico: '👁', color: 'var(--can)' },
{ page: 'motor.html', label: '电机控制', ico: '⚙', color: 'var(--motor)' },
{ page: 'continuity_test.html',label: '通断测试', ico: '🧰', color: 'var(--gd32)' },
{ page: 'spi_test.html', label: 'SPI 测速', ico: '⚡', color: 'var(--ble)' }
];
var bar = document.getElementById('tabbar');
var view = document.getElementById('view');
var spacer = bar.querySelector('.spacer');
var LS = 'dcu_tab';
function pageToTab(path){
path = (path || '').replace(/^.*\//, '').split('?')[0];
if (path === '' ) return 'index.html';
for (var i=0;i<TABS.length;i++) if (TABS[i].page === path) return path;
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);
}
}
function go(path, save){
view.src = path;
setActive(path);
if (save !== false) { try { localStorage.setItem(LS, path); } catch(e){} }
}
// 渲染标签
TABS.forEach(function(t){
var b = document.createElement('button');
b.className = 'tab'; b.type = 'button';
b.setAttribute('data-page', t.page);
b.innerHTML = '<span class="accent" style="background:'+t.color+'"></span>'
+ '<span class="ico">'+t.ico+'</span>' + t.label;
b.onclick = function(){ go(t.page); };
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 主页
var init = 'index.html';
try { var s = localStorage.getItem(LS); if (s && pageToTab(s)) init = s; } catch(e){}
go(init, false);
})();
</script>
</body>
</html>