feat: 完善 N7 智能座舱交互与控制链路
This commit is contained in:
+301
-40
@@ -17,7 +17,7 @@
|
||||
: "natural",
|
||||
activeModule: "self-orchestration",
|
||||
activePage: 0,
|
||||
sidebarCollapsed: sessionStorage.getItem("cognitive-agent.sidebar-collapsed") === "true",
|
||||
sidebarCollapsed: true,
|
||||
};
|
||||
|
||||
let reasoningPickerControl = null;
|
||||
@@ -580,13 +580,17 @@
|
||||
|
||||
function buildSidebar(commandPanel) {
|
||||
commandPanel.classList.add("v2-sidebar");
|
||||
commandPanel.id = "v2SidebarPanel";
|
||||
commandPanel.innerHTML = "";
|
||||
|
||||
const sidebarToggle = make("button", "v2-sidebar-toggle", '<i data-lucide="panel-left-close"></i>');
|
||||
const sidebarToggle = make("button", "v2-sidebar-toggle v2-panel-control-button", '<i data-lucide="panel-left"></i><span class="visually-hidden">左侧控制台</span>');
|
||||
sidebarToggle.id = "v2SidebarToggle";
|
||||
sidebarToggle.type = "button";
|
||||
sidebarToggle.setAttribute("aria-label", "折叠左侧控制台");
|
||||
sidebarToggle.setAttribute("aria-expanded", "true");
|
||||
sidebarToggle.setAttribute("aria-controls", commandPanel.id);
|
||||
sidebarToggle.setAttribute("aria-label", "展开左侧控制台");
|
||||
sidebarToggle.setAttribute("aria-pressed", "false");
|
||||
sidebarToggle.setAttribute("aria-expanded", "false");
|
||||
sidebarToggle.title = "展开左侧控制台";
|
||||
|
||||
const footer = make("footer", "v2-sidebar-footer", `
|
||||
<span class="v2-live-dot"></span>
|
||||
@@ -617,7 +621,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function buildOperation(mainPanel, originalForm, panelHeading, modeRail, requestHeader, gestureDrawer) {
|
||||
function buildOperation(mainPanel, originalForm, panelHeading, requestHeader, gestureDrawer) {
|
||||
const operation = make("section", "v2-operation-shell", `
|
||||
<div class="v2-operation-glow" aria-hidden="true"></div>
|
||||
<div class="v2-natural-panel" id="v2NaturalPanel"></div>
|
||||
@@ -628,7 +632,7 @@
|
||||
if (panelHeading) {
|
||||
panelHeading.classList.add("v2-operation-heading", "v2-module-heading");
|
||||
const title = $("h2", panelHeading);
|
||||
if (title) title.textContent = "一句话远程编排";
|
||||
if (title) title.textContent = "AI助手";
|
||||
naturalPanel.append(panelHeading);
|
||||
}
|
||||
|
||||
@@ -641,11 +645,6 @@
|
||||
naturalGrid.append(requestHeader);
|
||||
}
|
||||
naturalPanel.append(naturalGrid);
|
||||
if (modeRail) {
|
||||
modeRail.classList.add("v2-mode-rail");
|
||||
naturalPanel.append(modeRail);
|
||||
}
|
||||
|
||||
const gesturePanel = $("#v2GesturePanel");
|
||||
const gestureHeading = make("header", "v2-operation-heading v2-module-heading v2-gesture-operation-heading", `
|
||||
<h2>抬抬手,控制整车</h2>`);
|
||||
@@ -699,12 +698,16 @@
|
||||
<div><h1>511工程-智能座舱</h1></div>
|
||||
</div>
|
||||
<div class="v2-remote-control-state"><i data-lucide="radio-tower"></i><span>网页控制链路</span><strong>已启用</strong></div>
|
||||
<div class="v2-unified-live"><i></i><span>LIVE</span><b id="v2SyncClock">--:--:--</b></div>`;
|
||||
<div class="v2-unified-live"><i></i><span>LIVE</span><b id="v2SyncClock">--:--:--</b></div>
|
||||
<nav class="v2-panel-control-dock" id="v2PanelControlDock" aria-label="页面板块展开与收起"></nav>`;
|
||||
mainPanel.prepend(switcher);
|
||||
const panelDock = $("#v2PanelControlDock", switcher);
|
||||
const sidebarToggle = $("#v2SidebarToggle");
|
||||
if (panelDock && sidebarToggle) panelDock.append(sidebarToggle);
|
||||
}
|
||||
|
||||
|
||||
function buildOutput(mainPanel) {
|
||||
function buildOutput(mainPanel, modeRail) {
|
||||
const output = make("section", "v2-output-shell", `
|
||||
<header class="v2-output-heading">
|
||||
<div>
|
||||
@@ -717,6 +720,11 @@
|
||||
<nav class="v2-page-rail" id="v2PageRail" aria-label="信息分页" hidden></nav>`);
|
||||
mainPanel.append(output);
|
||||
|
||||
if (modeRail) {
|
||||
modeRail.classList.add("v2-mode-rail");
|
||||
mainPanel.insertBefore(modeRail, output);
|
||||
}
|
||||
|
||||
const content = $("#v2OutputContent");
|
||||
const twin = $(".joint-twin-shell", mainPanel);
|
||||
const resultWorkspace = $(".result-workspace", mainPanel);
|
||||
@@ -1165,14 +1173,17 @@
|
||||
function setSidePanelVisibility(key, visible) {
|
||||
const panel = key === "devices" ? $("#v2DevicePanel") : $("#v2ExtensionPanel");
|
||||
if (!panel) return;
|
||||
panel.hidden = false;
|
||||
const panelName = key === "devices" ? "车端设备状态" : "外设与扩展能力";
|
||||
panel.hidden = !visible;
|
||||
panel.classList.toggle("is-collapsed", !visible);
|
||||
panel.dataset.collapsed = String(!visible);
|
||||
sessionStorage.setItem(`cognitive-agent.sync-panel.${key}`, visible ? "visible" : "hidden");
|
||||
$$(`[data-panel-toggle="${key}"], [data-panel-close="${key}"]`).forEach((button) => {
|
||||
button.classList.toggle("active", visible);
|
||||
button.setAttribute("aria-pressed", String(visible));
|
||||
button.setAttribute("aria-label", `${visible ? "收起" : "展开"}${key === "devices" ? "车端设备状态" : "外设与扩展能力"}`);
|
||||
button.setAttribute("aria-expanded", String(visible));
|
||||
button.setAttribute("aria-label", `${visible ? "收起" : "展开"}${panelName}`);
|
||||
button.title = `${visible ? "收起" : "展开"}${panelName}`;
|
||||
const actionLabel = $("[data-panel-action-label]", button);
|
||||
if (actionLabel) actionLabel.textContent = visible ? "收起" : "展开";
|
||||
const stateLabel = $("[data-panel-state-label]", button);
|
||||
@@ -1183,9 +1194,234 @@
|
||||
return candidate && !candidate.classList.contains("is-collapsed");
|
||||
});
|
||||
$(".joint-twin-shell")?.setAttribute("data-open-panels", openPanels.join(" ") || "none");
|
||||
const rightRail = $(".v2-unified-right-rail");
|
||||
if (rightRail) {
|
||||
const collapsed = openPanels.length === 0;
|
||||
rightRail.hidden = collapsed;
|
||||
rightRail.classList.toggle("is-collapsed", collapsed);
|
||||
rightRail.toggleAttribute("inert", collapsed);
|
||||
rightRail.dataset.openPanels = openPanels.join(" ") || "none";
|
||||
}
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}
|
||||
|
||||
const twinPopoverClamp = (value, minimum, maximum) => (
|
||||
maximum < minimum ? minimum : Math.max(minimum, Math.min(maximum, value))
|
||||
);
|
||||
|
||||
function twinPopoverIntersectionArea(first, second) {
|
||||
const width = Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left));
|
||||
const height = Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top));
|
||||
return width * height;
|
||||
}
|
||||
|
||||
function positionTwinDevicePopover(deviceId = displaySync.activeDevice) {
|
||||
const stage = $("#seatTwin");
|
||||
const popover = $("#v2TwinDevicePopover");
|
||||
const hotspot = deviceId
|
||||
? $(`[data-twin-device="${deviceId}"]`, stage)
|
||||
: null;
|
||||
const hotspotLabel = hotspot ? $(".v2-twin-hotspot-label", hotspot) : null;
|
||||
if (!stage || !popover || popover.hidden || !hotspot || !hotspotLabel) return null;
|
||||
if (hotspot.hidden) {
|
||||
popover.dataset.anchorVisible = "false";
|
||||
if (popover.dataset.pinned === "false") popover.hidden = true;
|
||||
return null;
|
||||
}
|
||||
popover.dataset.anchorVisible = "true";
|
||||
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const labelClientRect = hotspotLabel.getBoundingClientRect();
|
||||
const popoverWidth = popover.offsetWidth;
|
||||
const popoverHeight = popover.offsetHeight;
|
||||
if (!stageRect.width || !stageRect.height || !popoverWidth || !popoverHeight) return null;
|
||||
|
||||
const margin = stageRect.width < 520 ? 8 : 12;
|
||||
const gap = stageRect.width < 520 ? 8 : 12;
|
||||
const bounds = {
|
||||
left: margin,
|
||||
top: margin,
|
||||
right: Math.max(margin, stageRect.width - margin),
|
||||
bottom: Math.max(margin, stageRect.height - margin),
|
||||
};
|
||||
const labelRect = {
|
||||
left: labelClientRect.left - stageRect.left,
|
||||
top: labelClientRect.top - stageRect.top,
|
||||
right: labelClientRect.right - stageRect.left,
|
||||
bottom: labelClientRect.bottom - stageRect.top,
|
||||
};
|
||||
labelRect.width = labelRect.right - labelRect.left;
|
||||
labelRect.height = labelRect.bottom - labelRect.top;
|
||||
labelRect.centerX = (labelRect.left + labelRect.right) / 2;
|
||||
labelRect.centerY = (labelRect.top + labelRect.bottom) / 2;
|
||||
|
||||
const toStageRect = (element, padding = 0) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left - stageRect.left - padding,
|
||||
top: rect.top - stageRect.top - padding,
|
||||
right: rect.right - stageRect.left + padding,
|
||||
bottom: rect.bottom - stageRect.top + padding,
|
||||
};
|
||||
};
|
||||
const visibleElement = (element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const labelObstacles = $$(".v2-twin-hotspot-label", stage)
|
||||
.filter((element) => element !== hotspotLabel && visibleElement(element))
|
||||
.map((element) => ({
|
||||
deviceId: element.closest("[data-twin-device]")?.dataset.twinDevice || "",
|
||||
rect: toStageRect(element, 3),
|
||||
}));
|
||||
const chromeObstacles = $$([".twin-seat-label", ".twin-orientation", ".twin-readout:not([hidden])"].join(","), stage)
|
||||
.filter(visibleElement)
|
||||
.map((element) => toStageRect(element, 5));
|
||||
const activeLabelObstacle = {
|
||||
left: labelRect.left - 10,
|
||||
top: labelRect.top - 10,
|
||||
right: labelRect.right + 10,
|
||||
bottom: labelRect.bottom + 10,
|
||||
};
|
||||
const geometrySignature = [
|
||||
deviceId,
|
||||
popover.dataset.pinned,
|
||||
Math.round(stageRect.width),
|
||||
Math.round(stageRect.height),
|
||||
Math.round(popoverWidth),
|
||||
Math.round(popoverHeight),
|
||||
Math.round(labelRect.left),
|
||||
Math.round(labelRect.top),
|
||||
...labelObstacles.flatMap(({ deviceId: obstacleDeviceId, rect }) => [
|
||||
obstacleDeviceId,
|
||||
Math.round(rect.left),
|
||||
Math.round(rect.top),
|
||||
]),
|
||||
...chromeObstacles.flatMap((rect) => [Math.round(rect.left), Math.round(rect.top)]),
|
||||
].join(":");
|
||||
if (popover.dataset.layoutSignature === geometrySignature) return null;
|
||||
popover.dataset.layoutSignature = geometrySignature;
|
||||
|
||||
const availableSpace = {
|
||||
right: bounds.right - labelRect.right,
|
||||
left: labelRect.left - bounds.left,
|
||||
bottom: bounds.bottom - labelRect.bottom,
|
||||
top: labelRect.top - bounds.top,
|
||||
};
|
||||
const sideOrder = Object.keys(availableSpace).sort((first, second) => (
|
||||
availableSpace[second] - availableSpace[first]
|
||||
));
|
||||
const rawCandidates = [];
|
||||
const pushSideCandidates = (side) => {
|
||||
const horizontal = side === "left" || side === "right";
|
||||
const alignments = horizontal
|
||||
? [
|
||||
[labelRect.centerY - popoverHeight / 2, "center"],
|
||||
[labelRect.top, "start"],
|
||||
[labelRect.bottom - popoverHeight, "end"],
|
||||
]
|
||||
: [
|
||||
[labelRect.centerX - popoverWidth / 2, "center"],
|
||||
[labelRect.left, "start"],
|
||||
[labelRect.right - popoverWidth, "end"],
|
||||
];
|
||||
alignments.forEach(([crossAxis, alignment], alignmentIndex) => {
|
||||
let left = crossAxis;
|
||||
let top = crossAxis;
|
||||
if (side === "right") {
|
||||
left = labelRect.right + gap;
|
||||
top = crossAxis;
|
||||
} else if (side === "left") {
|
||||
left = labelRect.left - gap - popoverWidth;
|
||||
top = crossAxis;
|
||||
} else if (side === "bottom") {
|
||||
left = crossAxis;
|
||||
top = labelRect.bottom + gap;
|
||||
} else {
|
||||
left = crossAxis;
|
||||
top = labelRect.top - gap - popoverHeight;
|
||||
}
|
||||
rawCandidates.push({ left, top, side, alignment, alignmentIndex, anchored: true });
|
||||
});
|
||||
};
|
||||
sideOrder.forEach(pushSideCandidates);
|
||||
|
||||
const maximumLeft = bounds.right - popoverWidth;
|
||||
const maximumTop = bounds.bottom - popoverHeight;
|
||||
[bounds.left, labelRect.left - gap - popoverWidth, labelRect.centerX - popoverWidth / 2,
|
||||
labelRect.right + gap, maximumLeft].forEach((left) => {
|
||||
[bounds.top, labelRect.top - gap - popoverHeight, labelRect.centerY - popoverHeight / 2,
|
||||
labelRect.bottom + gap, maximumTop].forEach((top) => {
|
||||
rawCandidates.push({ left, top, side: "adaptive", alignment: "free", alignmentIndex: 0, anchored: false });
|
||||
});
|
||||
});
|
||||
|
||||
const seen = new Set();
|
||||
const previousPlacement = popover.dataset.anchorDevice === deviceId
|
||||
? `${popover.dataset.placement || ""}:${popover.dataset.alignment || ""}`
|
||||
: "";
|
||||
const candidates = rawCandidates.map((candidate) => {
|
||||
const left = twinPopoverClamp(candidate.left, bounds.left, maximumLeft);
|
||||
const top = twinPopoverClamp(candidate.top, bounds.top, maximumTop);
|
||||
const key = `${Math.round(left)}:${Math.round(top)}`;
|
||||
if (seen.has(key)) return null;
|
||||
seen.add(key);
|
||||
const rect = {
|
||||
left,
|
||||
top,
|
||||
right: left + popoverWidth,
|
||||
bottom: top + popoverHeight,
|
||||
};
|
||||
const clampDistance = Math.abs(left - candidate.left) + Math.abs(top - candidate.top);
|
||||
const activeOverlap = twinPopoverIntersectionArea(rect, activeLabelObstacle);
|
||||
const shadeOverlap = labelObstacles.reduce((total, obstacle) => (
|
||||
total + (obstacle.deviceId === "shade" ? twinPopoverIntersectionArea(rect, obstacle.rect) : 0)
|
||||
), 0);
|
||||
const labelOverlap = labelObstacles.reduce((total, obstacle) => (
|
||||
total + (obstacle.deviceId === "shade" ? 0 : twinPopoverIntersectionArea(rect, obstacle.rect))
|
||||
), 0);
|
||||
const chromeOverlap = chromeObstacles.reduce((total, obstacle) => (
|
||||
total + twinPopoverIntersectionArea(rect, obstacle)
|
||||
), 0);
|
||||
const distance = Math.hypot(
|
||||
left + popoverWidth / 2 - labelRect.centerX,
|
||||
top + popoverHeight / 2 - labelRect.centerY,
|
||||
);
|
||||
const placement = candidate.side === "adaptive"
|
||||
? (Math.abs(left + popoverWidth / 2 - labelRect.centerX) > Math.abs(top + popoverHeight / 2 - labelRect.centerY)
|
||||
? (left + popoverWidth / 2 >= labelRect.centerX ? "right" : "left")
|
||||
: (top + popoverHeight / 2 >= labelRect.centerY ? "bottom" : "top"))
|
||||
: candidate.side;
|
||||
const placementKey = `${placement}:${candidate.alignment}`;
|
||||
const sideRank = sideOrder.indexOf(placement);
|
||||
const score = activeOverlap * 1000
|
||||
+ shadeOverlap * 640
|
||||
+ labelOverlap * 36
|
||||
+ chromeOverlap * 12
|
||||
+ clampDistance * 90
|
||||
+ distance * 0.35
|
||||
+ Math.max(0, sideRank) * 70
|
||||
+ candidate.alignmentIndex * 8
|
||||
+ (candidate.anchored ? 0 : 90)
|
||||
- (placementKey === previousPlacement ? 28 : 0);
|
||||
return { ...candidate, left, top, placement, score, activeOverlap, shadeOverlap, labelOverlap };
|
||||
}).filter(Boolean);
|
||||
const best = candidates.sort((first, second) => first.score - second.score)[0];
|
||||
if (!best) return null;
|
||||
|
||||
popover.style.left = `${Math.round(best.left)}px`;
|
||||
popover.style.top = `${Math.round(best.top)}px`;
|
||||
popover.style.removeProperty("right");
|
||||
popover.style.removeProperty("bottom");
|
||||
popover.dataset.anchorDevice = deviceId;
|
||||
popover.dataset.placement = best.placement;
|
||||
popover.dataset.alignment = best.alignment;
|
||||
popover.dataset.shadeOverlap = String(Math.round(best.shadeOverlap));
|
||||
popover.dataset.labelOverlap = String(Math.round(best.labelOverlap));
|
||||
return best;
|
||||
}
|
||||
|
||||
function presentTwinDevice(deviceId, { pinned = false, selectSeat = pinned } = {}) {
|
||||
const device = syncedDevices.find((item) => item.id === deviceId);
|
||||
const popover = $("#v2TwinDevicePopover");
|
||||
@@ -1209,6 +1445,7 @@
|
||||
$(`[data-twin-seat="${deviceId}"]`)?.click();
|
||||
}
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
positionTwinDevicePopover(deviceId);
|
||||
}
|
||||
|
||||
function clearTwinDevicePreview({ force = false } = {}) {
|
||||
@@ -1323,7 +1560,8 @@
|
||||
const utterance = String(task.utterance || "");
|
||||
const result = task.result && typeof task.result === "object" ? task.result : null;
|
||||
const input = $("#utterance");
|
||||
if (input && utterance && document.activeElement !== input) input.value = utterance;
|
||||
if (input && utterance && document.activeElement !== input
|
||||
&& !document.body.classList.contains("assistant-chat-enabled")) input.value = utterance;
|
||||
const kind = taskKind(utterance, result, String(task.source || ""));
|
||||
if ($("#v2TaskKind")) $("#v2TaskKind").textContent = kind;
|
||||
if ($("#v2TaskCommand")) $("#v2TaskCommand").textContent = utterance || "等待车机端或网页端选择预设模式、输入一句话指令";
|
||||
@@ -1447,14 +1685,14 @@
|
||||
if (input) {
|
||||
input.readOnly = false;
|
||||
input.removeAttribute("aria-readonly");
|
||||
input.setAttribute("aria-label", "输入一句话远程编排车端功能");
|
||||
input.placeholder = "例如:副驾调到舒适姿态,空调 24℃,氛围灯改成暖橙色";
|
||||
input.setAttribute("aria-label", "输入问题或座舱控制需求");
|
||||
input.placeholder = "输入问题或座舱控制需求";
|
||||
}
|
||||
form?.addEventListener("submit", () => {
|
||||
const utterance = input?.value.trim();
|
||||
if (utterance) appendSyncLog({
|
||||
category: "task",
|
||||
title: "网页端发送编排指令",
|
||||
title: "网页端发送助手请求",
|
||||
detail: utterance,
|
||||
status: "info",
|
||||
});
|
||||
@@ -1468,7 +1706,7 @@
|
||||
function syncSidebarLogBaseline() {
|
||||
const sidebar = $(".v2-sidebar");
|
||||
const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band");
|
||||
if (!sidebar || !lowerPanel) return;
|
||||
if (!sidebar || !lowerPanel || ui.sidebarCollapsed) return;
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
const overview = $(".v2-sync-sidebar-overview", sidebar);
|
||||
const overviewRect = overview?.getBoundingClientRect();
|
||||
@@ -1539,8 +1777,6 @@
|
||||
});
|
||||
}
|
||||
syncOverview?.append(sidebarFooter);
|
||||
const sidebarToggle = $("#v2SidebarToggle", sidebar);
|
||||
if (sidebarToggle) $(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);
|
||||
}
|
||||
|
||||
const naturalPanel = $("#v2NaturalPanel");
|
||||
@@ -1554,16 +1790,34 @@
|
||||
const cabinBand = $(".cabin-band", jointShell);
|
||||
if (jointShell && cabinBand) {
|
||||
const rightRail = make("aside", "v2-unified-right-rail", `
|
||||
<section class="v2-device-sync-board v2-side-status-panel" id="v2DevicePanel">
|
||||
<header><div><h3>车端设备状态</h3></div><span><i></i>实时反馈</span><button type="button" data-panel-toggle="devices" aria-label="收起车端设备状态"><i data-lucide="panel-left-close"></i><span data-panel-action-label>收起</span></button></header>
|
||||
<div class="v2-device-sync-grid" id="v2DeviceSyncGrid"></div>
|
||||
</section>
|
||||
<section class="v2-extension-panel v2-side-status-panel" id="v2ExtensionPanel"><div class="v2-extension-sync-mount" id="vehicleExtensionSync" aria-live="polite"></div></section>`);
|
||||
<div class="v2-side-status-stack">
|
||||
<section class="v2-device-sync-board v2-side-status-panel" id="v2DevicePanel">
|
||||
<header><div><h3>车端设备状态</h3></div><span><i></i>实时反馈</span></header>
|
||||
<div class="v2-device-sync-grid" id="v2DeviceSyncGrid"></div>
|
||||
</section>
|
||||
<section class="v2-extension-panel v2-side-status-panel" id="v2ExtensionPanel"><div class="v2-extension-sync-mount" id="vehicleExtensionSync" aria-live="polite"></div></section>
|
||||
</div>`);
|
||||
jointShell.insertBefore(rightRail, cabinBand);
|
||||
const panelControls = make("div", "v2-panel-visibility-controls", `
|
||||
<button type="button" data-panel-toggle="devices" aria-pressed="true"><i data-lucide="sliders-horizontal"></i><span>设备状态</span><b data-panel-state-label>已展开</b></button>
|
||||
<button type="button" data-panel-toggle="extensions" aria-pressed="true"><i data-lucide="usb"></i><span>外设能力</span><b data-panel-state-label>已展开</b></button>`);
|
||||
$(".twin-toolbar", jointShell)?.prepend(panelControls);
|
||||
const panelDock = $("#v2PanelControlDock");
|
||||
if (panelDock) {
|
||||
const deviceToggle = make("button", "v2-panel-control-button", '<i data-lucide="car-front"></i><span class="visually-hidden">车端设备状态</span>');
|
||||
deviceToggle.type = "button";
|
||||
deviceToggle.dataset.panelToggle = "devices";
|
||||
deviceToggle.setAttribute("aria-controls", "v2DevicePanel");
|
||||
deviceToggle.setAttribute("aria-pressed", "false");
|
||||
deviceToggle.setAttribute("aria-expanded", "false");
|
||||
deviceToggle.setAttribute("aria-label", "展开车端设备状态");
|
||||
deviceToggle.title = "展开车端设备状态";
|
||||
const extensionToggle = make("button", "v2-panel-control-button", '<i data-lucide="usb"></i><span class="visually-hidden">外设与扩展能力</span>');
|
||||
extensionToggle.type = "button";
|
||||
extensionToggle.dataset.panelToggle = "extensions";
|
||||
extensionToggle.setAttribute("aria-controls", "v2ExtensionPanel");
|
||||
extensionToggle.setAttribute("aria-pressed", "false");
|
||||
extensionToggle.setAttribute("aria-expanded", "false");
|
||||
extensionToggle.setAttribute("aria-label", "展开外设与扩展能力");
|
||||
extensionToggle.title = "展开外设与扩展能力";
|
||||
panelDock.append(deviceToggle, extensionToggle);
|
||||
}
|
||||
buildTwinDeviceInteractions();
|
||||
cabinBand.classList.add("v2-source-state-only");
|
||||
new MutationObserver(scheduleSyncedDeviceRender).observe(cabinBand, {
|
||||
@@ -1580,8 +1834,8 @@
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
setSidePanelVisibility("devices", panelVisible("devices"));
|
||||
setSidePanelVisibility("extensions", panelVisible("extensions"));
|
||||
setSidePanelVisibility("devices", false);
|
||||
setSidePanelVisibility("extensions", false);
|
||||
window.addEventListener("resize", syncSidebarLogBaseline);
|
||||
requestAnimationFrame(syncSidebarLogBaseline);
|
||||
document.addEventListener("click", (event) => {
|
||||
@@ -1842,14 +2096,21 @@
|
||||
String(ui.sidebarCollapsed),
|
||||
);
|
||||
document.body.classList.toggle("v2-sidebar-collapsed", ui.sidebarCollapsed);
|
||||
const sidebar = $("#v2SidebarPanel");
|
||||
if (sidebar) {
|
||||
sidebar.setAttribute("aria-hidden", String(ui.sidebarCollapsed));
|
||||
sidebar.toggleAttribute("inert", ui.sidebarCollapsed);
|
||||
}
|
||||
const button = $("#v2SidebarToggle");
|
||||
if (button) {
|
||||
const expanded = !ui.sidebarCollapsed;
|
||||
button.classList.toggle("active", expanded);
|
||||
button.setAttribute("aria-pressed", String(expanded));
|
||||
button.setAttribute("aria-expanded", String(!ui.sidebarCollapsed));
|
||||
button.setAttribute(
|
||||
"aria-label",
|
||||
ui.sidebarCollapsed ? "展开左侧控制台" : "折叠左侧控制台",
|
||||
);
|
||||
button.innerHTML = `<i data-lucide="${ui.sidebarCollapsed ? "panel-left-open" : "panel-left-close"}"></i><span>${ui.sidebarCollapsed ? "展开" : "收起"}</span>`;
|
||||
const label = ui.sidebarCollapsed ? "展开左侧控制台" : "收起左侧控制台";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = label;
|
||||
button.innerHTML = '<i data-lucide="panel-left"></i><span class="visually-hidden">左侧控制台</span>';
|
||||
}
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
|
||||
@@ -1959,12 +2220,11 @@
|
||||
mainPanel,
|
||||
originalForm,
|
||||
panelHeading,
|
||||
modeRail,
|
||||
requestHeader,
|
||||
gestureDrawer,
|
||||
);
|
||||
buildModuleSwitcher(mainPanel);
|
||||
buildOutput(mainPanel);
|
||||
buildOutput(mainPanel, modeRail);
|
||||
buildSyncControlLayer();
|
||||
bindEvents();
|
||||
setControlMode(ui.controlMode, { triggerGesture: false });
|
||||
@@ -1983,6 +2243,7 @@
|
||||
getModule: () => ui.activeModule,
|
||||
getRole: () => "user",
|
||||
getParsedCanCommands: (result) => parsedCanCommands(result).map((item) => ({ ...item })),
|
||||
positionTwinDevicePopover,
|
||||
setModule,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user