feat(n7web): refine spatial UI and NFC-gated extension routing

This commit is contained in:
Miayui
2026-09-16 16:14:10 +08:00
parent 0e4328f548
commit 75ce391312
16 changed files with 925 additions and 106 deletions
+82 -18
View File
@@ -568,7 +568,11 @@
const verificationStatus = String(telemetry.verification_status || "").toLowerCase();
const failureMessage = String(execution?.message || "").trim();
const aborted = abortedStates.has(executionStatus);
const explicitlyBlocked = executionStatus === "blocked"
|| verificationStatus === "blocked"
|| Boolean(telemetry.authorization_reason);
const executionFailed = executionStatus === "failed"
|| explicitlyBlocked
|| verificationStatus === "failed_or_blocked";
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
@@ -579,7 +583,8 @@
|| /未收到.*(?:有效|新鲜).*遮阳帘.*位置/.test(failureMessage)
);
const safetyBlocked = executionFailed && !submitted && (
missingWindowFeedback
explicitlyBlocked
|| missingWindowFeedback
|| missingShadeFeedback
|| /refusing (?:blind )?motion/i.test(failureMessage)
|| /拒绝(?:启动机构|盲动|执行)/.test(failureMessage)
@@ -1100,9 +1105,9 @@
sidebarToggle.setAttribute("aria-expanded", "false");
sidebarToggle.title = "展开左侧控制台";
const footer = make("footer", "v2-sidebar-footer", `
const footer = make("footer", "v2-sidebar-footer v2-control-link-card", `
<span class="v2-live-dot"></span>
<div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div>
<div><strong>智舱控制链路</strong><small id="v2ConnectionText" aria-live="polite">正在连接服务</small></div>
<button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`);
commandPanel.append(sidebarToggle, footer);
@@ -1675,6 +1680,11 @@
if (!list) return;
const filter = $("#v2LogFilter")?.value || "all";
const items = displaySync.logs.filter((item) => filter === "all" || item.category === filter);
const count = $("#v2SyncLogCount");
if (count) {
count.textContent = String(items.length);
count.title = `${items.length} 条记录`;
}
const icons = { task: "route", sync: "refresh-cw", extension: "usb", device: "activity" };
list.innerHTML = items.length ? items.map((item) => `
<li data-log-status="${escapeHtml(item.status)}">
@@ -1685,6 +1695,21 @@
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function setSyncLogCollapsed(collapsed) {
const panel = $(".v2-sync-log");
const list = $("#v2SyncLogList");
const toggle = $("#v2SyncLogToggle");
if (!panel || !list || !toggle) return;
const nextCollapsed = Boolean(collapsed);
const label = nextCollapsed ? "展开运行日志与同步记录" : "折叠运行日志与同步记录";
panel.dataset.collapsed = String(nextCollapsed);
list.hidden = nextCollapsed;
toggle.setAttribute("aria-expanded", String(!nextCollapsed));
toggle.setAttribute("aria-label", label);
toggle.title = label;
requestAnimationFrame(syncSidebarLogBaseline);
}
function panelVisible(key) {
const panel = key === "devices" ? $("#v2DevicePanel") : $("#v2ExtensionPanel");
if (panel) return !panel.classList.contains("is-collapsed");
@@ -2439,9 +2464,15 @@
const sidebarFooter = $(".v2-sidebar-footer", sidebar);
const syncSidebar = make("section", "v2-sync-sidebar", `
<div class="v2-sync-sidebar-overview"></div>
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
<header><div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
<ol id="v2SyncLogList" aria-live="polite"></ol>
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle" data-collapsed="true">
<header>
<div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div>
<div class="v2-sync-log-actions">
<select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select>
<button type="button" class="v2-sync-log-toggle" id="v2SyncLogToggle" aria-controls="v2SyncLogList" aria-expanded="false" aria-label="展开运行日志与同步记录" title="展开运行日志与同步记录"><span id="v2SyncLogCount">0</span><i data-lucide="chevron-down"></i></button>
</div>
</header>
<ol id="v2SyncLogList" aria-live="polite" hidden></ol>
</section>`);
if (sidebar && sidebarFooter) {
sidebar.insertBefore(syncSidebar, sidebarFooter);
@@ -2574,6 +2605,10 @@
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
});
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
$("#v2SyncLogToggle")?.addEventListener("click", () => {
setSyncLogCollapsed($(".v2-sync-log")?.dataset.collapsed !== "true");
});
setSyncLogCollapsed(true);
const beginPendingControlRequest = (request = {}) => {
beginCommandTiming(request);
resetValidatedCommandContext();
@@ -2879,18 +2914,38 @@
const connectionText = $("#v2ConnectionText");
if (connectionText) {
const status = connectionState?.status || "connecting";
const labels = {
connecting: "正在连接服务",
online: "服务已连接",
degraded: "连接不稳定",
offline: "服务已断开",
};
connectionText.textContent = labels[status] || labels.connecting;
connectionText.title = connectionState?.lastError || connectionText.textContent;
$(".v2-sidebar-footer")?.classList.toggle(
"offline",
status === "offline" || status === "degraded",
);
const selectedTransport = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|| $('.vehicle-transport-options [data-control-transport].active');
const transport = selectedTransport?.dataset.controlTransport || "";
const transportOnline = selectedTransport?.dataset.connectionState === "online";
const transportLabel = transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
const footer = connectionText.closest(".v2-control-link-card");
let presentationState = status;
let label = "正在连接服务";
if (status === "online" && transport) {
presentationState = transportOnline ? "online" : "offline";
label = transport === "wireless_mqtt"
? `${transportLabel} · MQTT ${transportOnline ? "已连接" : "未连接"}`
: `${transportLabel} · 信号链路${transportOnline ? "正常" : "断开"}`;
} else if (status === "online") {
presentationState = "online";
label = "服务已连接";
} else if (status === "degraded") {
label = transport ? `${transportLabel} · 主服务连接不稳定` : "主服务连接不稳定";
} else if (status === "offline") {
label = transport ? `${transportLabel} · 主服务已断开` : "主服务已断开";
}
connectionText.textContent = label;
connectionText.title = connectionState?.lastError
|| (!transportOnline ? selectedTransport?.title : "")
|| label;
if (footer) {
footer.dataset.transport = transport || "service";
footer.dataset.state = presentationState;
footer.classList.toggle("offline", presentationState !== "online");
}
}
keepRunButtonAvailable();
}
@@ -2927,6 +2982,7 @@
syncRuntimePresentation(event.detail);
});
window.addEventListener("cognitive-agent:vehicle-feedback-state", () => {
syncRuntimePresentation();
renderSyncedDevices();
if (displaySync.activeDevice) {
presentTwinDevice(displaySync.activeDevice, {
@@ -2935,6 +2991,14 @@
});
}
});
const transportOptions = $(".vehicle-transport-options");
if (transportOptions) {
new MutationObserver(() => syncRuntimePresentation()).observe(transportOptions, {
attributes: true,
attributeFilter: ["aria-pressed", "class", "data-connection-state", "title"],
subtree: true,
});
}
const observer = new MutationObserver(() => keepRunButtonAvailable());
[$("#hardwareMode"), $("#simulationMode")]
.filter(Boolean)