update docker

This commit is contained in:
2026-06-13 10:17:26 +07:00
parent 4f8d8148f7
commit 8c111f2406
11 changed files with 431 additions and 19 deletions

View File

@@ -3,6 +3,15 @@ const el = (id) => document.getElementById(id);
const statusEl = el("status");
const listEl = el("lidarList");
const lidarFormHintEl = el("lidarFormHint");
const pageTitleEl = document.querySelector(".pageTitle");
const navItemEls = Array.from(document.querySelectorAll(".navItem[data-page]"));
const pageOverviewEl = el("pageOverview");
const pageConfigEl = el("pageConfig");
const overviewBackendEl = el("overviewBackend");
const overviewActiveLayoutEl = el("overviewActiveLayout");
const overviewActiveModelEl = el("overviewActiveModel");
const overviewActiveSensorsEl = el("overviewActiveSensors");
const configSplitterEl = el("configSplitter");
const canvasWrap = el("canvasWrap");
const robotModelEl = el("robotModel");
@@ -107,6 +116,111 @@ const state = {
pendingFootprintClick: null, // { sx, sy } when Shift+click may add a vertex
};
function setActivePage(page) {
const p = page === "overview" ? "overview" : "config";
navItemEls.forEach((a) => {
const on = (a.dataset.page || "") === p;
a.classList.toggle("active", on);
if (on) a.setAttribute("aria-current", "page");
else a.removeAttribute("aria-current");
});
if (pageTitleEl) pageTitleEl.textContent = p === "overview" ? "Tổng quan" : "Cấu Hình";
if (pageOverviewEl) pageOverviewEl.hidden = p !== "overview";
if (pageConfigEl) pageConfigEl.hidden = p !== "config";
try {
localStorage.setItem("activePage", p);
} catch {
/* ignore */
}
}
function initNavigation() {
navItemEls.forEach((a) => {
a.addEventListener("click", (evt) => {
evt.preventDefault();
setActivePage(a.dataset.page || "config");
});
});
// Restore last page, default to config (màn hình chính).
let initial = "config";
try {
const saved = localStorage.getItem("activePage");
if (saved === "overview" || saved === "config") initial = saved;
} catch {
/* ignore */
}
setActivePage(initial);
}
function setLeftPaneWidth(px) {
const v = Math.round(clamp(Number(px), 320, 720));
document.documentElement.style.setProperty("--leftPaneW", `${v}px`);
try {
localStorage.setItem("leftPaneW", String(v));
} catch {
/* ignore */
}
}
function initSplitPane() {
if (!configSplitterEl) return;
try {
const saved = Number(localStorage.getItem("leftPaneW"));
if (Number.isFinite(saved) && saved > 0) setLeftPaneWidth(saved);
else setLeftPaneWidth(460);
} catch {
setLeftPaneWidth(460);
}
let dragging = false;
let startX = 0;
let startW = 0;
const onMove = (evt) => {
if (!dragging) return;
const x = evt.clientX ?? (evt.touches && evt.touches[0] ? evt.touches[0].clientX : startX);
setLeftPaneWidth(startW + (x - startX));
};
const onUp = () => {
if (!dragging) return;
dragging = false;
configSplitterEl.classList.remove("dragging");
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
window.removeEventListener("touchmove", onMove);
window.removeEventListener("touchend", onUp);
};
configSplitterEl.addEventListener("mousedown", (evt) => {
evt.preventDefault();
dragging = true;
configSplitterEl.classList.add("dragging");
startX = evt.clientX;
startW = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
});
configSplitterEl.addEventListener("touchstart", (evt) => {
if (!evt.touches || !evt.touches[0]) return;
dragging = true;
configSplitterEl.classList.add("dragging");
startX = evt.touches[0].clientX;
startW = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
window.addEventListener("touchmove", onMove, { passive: true });
window.addEventListener("touchend", onUp);
}, { passive: false });
// Keyboard resize (focus splitter, use arrows)
configSplitterEl.addEventListener("keydown", (evt) => {
if (evt.key !== "ArrowLeft" && evt.key !== "ArrowRight") return;
evt.preventDefault();
const cur = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
setLeftPaneWidth(cur + (evt.key === "ArrowLeft" ? -20 : 20));
});
}
const DIFF_DEFAULTS = {
frame_id: "base_footprint",
wheel_separation_m: 1.0,
@@ -2906,6 +3020,7 @@ async function loadAll() {
loadAllInFlight = (async () => {
const st = await api("/api/state");
if (overviewBackendEl) overviewBackendEl.textContent = "OK";
state.activeLayoutId = st.active_layout_id || null;
state.activeLayoutName = st.active_layout_name || "";
state.layoutCatalog = st.layouts || [];
@@ -2980,6 +3095,16 @@ async function loadAll() {
setSelectedRelText();
renderList();
renderImuList();
if (overviewActiveLayoutEl) {
const name = state.activeLayoutName || state.activeLayoutId || "—";
overviewActiveLayoutEl.textContent = name;
}
if (overviewActiveModelEl) {
overviewActiveModelEl.textContent = state.layout?.robot?.model || "diff";
}
if (overviewActiveSensorsEl) {
overviewActiveSensorsEl.textContent = `${state.lidars.length} LiDAR • ${state.imus.length} IMU`;
}
if (!state.viewInitialized) {
fitViewToWorld();
state.viewInitialized = true;
@@ -3203,6 +3328,8 @@ function initRobotModelPanelCollapse() {
}
initLayoutManagerEvents();
initNavigation();
initSplitPane();
initLidarForm();
initMotorWheelsEvents();
initBicycleMotorWheelsEvents();
@@ -3253,6 +3380,7 @@ saveLayoutBtn.addEventListener("click", async () => {
setStatus("Sẵn sàng");
} catch (e) {
const msg = String(e.message || e);
if (overviewBackendEl) overviewBackendEl.textContent = `Lỗi: ${msg}`;
if (msg.includes("stack") || msg.includes("Maximum call")) {
setStatus(`Lỗi JavaScript: ${msg}`);
} else {

View File

@@ -50,6 +50,51 @@
</header>
<main class="content">
<div class="page" id="pageOverview" data-page-content="overview" hidden>
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Phần mềm</div>
<div class="cardSub">Thông tin phiên bản và trạng thái backend.</div>
</div>
</div>
<div class="cardBody">
<div class="row rowWide">
<label>Backend</label>
<div id="overviewBackend" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>Dữ liệu</label>
<div class="mutedNote">`data/state.json` (catalog) + `data/models/{id}.json` (layout)</div>
</div>
</div>
</section>
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Cấu hình đang active</div>
<div class="cardSub">Tóm tắt layout đang mở.</div>
</div>
</div>
<div class="cardBody">
<div class="row rowWide">
<label>Layout</label>
<div id="overviewActiveLayout" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>Model robot</label>
<div id="overviewActiveModel" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>LiDAR / IMU</label>
<div id="overviewActiveSensors" class="mutedNote"></div>
</div>
</div>
</section>
</div>
<div class="page" id="pageConfig" data-page-content="config">
<div class="contentLeft">
<section class="card" id="layoutManagerCard">
<div class="cardHeader">
@@ -462,26 +507,32 @@
</section>
</div>
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Bố trí trên robot</div>
</div>
</div>
</div>
<div class="cardBody">
<div class="canvasWrap" id="canvasWrap">
<canvas id="canvas"></canvas>
<div id="configSplitter" class="splitter" role="separator" aria-orientation="vertical" tabindex="0"></div>
<div class="contentRight">
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Bố trí trên robot</div>
</div>
</div>
<div class="metaBar">
<div class="viewHint">Cuộn chuột: zoom • Shift + kéo: di chuyển vùng nhìn</div>
<div id="robotDiffSummary" class="robotDiffSummary"></div>
<div>Robot center: <span id="robotCenterText"></span></div>
<div>Selected: <span id="selectedText">none</span></div>
<div>Pose: <span id="selectedRelText"></span></div>
<div class="cardBody">
<div class="canvasWrap" id="canvasWrap">
<canvas id="canvas"></canvas>
</div>
<div class="metaBar">
<div class="viewHint">Cuộn chuột: zoom • Shift + kéo: di chuyển vùng nhìn</div>
<div id="robotDiffSummary" class="robotDiffSummary"></div>
<div>Robot center: <span id="robotCenterText"></span></div>
<div>Selected: <span id="selectedText">none</span></div>
<div>Pose: <span id="selectedRelText"></span></div>
</div>
</div>
</div>
</section>
</section>
</div>
</main>
</div>
</div>

View File

@@ -137,15 +137,38 @@ body {
.content {
padding: 18px;
display: grid;
grid-template-columns: min(460px, 100%) 1fr;
grid-template-columns: var(--leftPaneW, 460px) 10px 1fr;
gap: 16px;
align-items: start;
height: 100%;
min-height: 0;
}
.contentLeft {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
max-height: 100%;
overflow: auto;
overscroll-behavior: contain;
}
.contentRight {
min-width: 0;
max-height: 100%;
}
.splitter {
align-self: stretch;
width: 10px;
margin: 0 -16px; /* overlap gap a bit for easier grab */
cursor: col-resize;
border-radius: 10px;
background: rgba(15, 23, 42, 0.04);
border: 1px solid rgba(15, 23, 42, 0.08);
}
.splitter:hover,
.splitter.dragging {
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.25);
}
.modelForm { display: grid; gap: 10px; }
.modelParams { display: grid; gap: 10px; }
@@ -519,6 +542,8 @@ canvas {
.shell { grid-template-columns: 1fr; }
.sidebar { position: relative; height: auto; }
.body { grid-template-rows: auto 1fr; }
.content { grid-template-columns: 1fr; }
.content { grid-template-columns: 1fr; height: auto; }
.splitter { display: none; }
.contentLeft { max-height: none; overflow: visible; }
}