Files
ESP32_COMM/tools/js/can/persist.js
T
小龙虾 33b41fd50a docs(frontend): 添加前端架构约定与回归清单文档
添加了完整的前端架构约定文档 (frontend_architecture.md),
包含以下主要内容:

- 架构总览:Web资源本地托管方式和零构建方案
- 页面薄入口范式:HTML结构与ES Modules入口约定
- JS分层架构:common/ota/ble/can/motor等目录结构说明
- 缓存戳约定:版本管理强制要求
- 公共层API契约:baseUrl/toast/polling等接口规范
- motor页特殊性:独立主题和轮询链路处理
- 红线区域:不可动代码范围说明
- 回归清单:各页面功能验证checklist

同时移除了build相关的临时文件
2026-07-27 01:50:48 +08:00

223 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* persist.js - CAN 控制台会话持久化(刷新后恢复)
*
* 保存到 localStorage:多 DBC 原文、信号编辑值、勾选/展开、发送节点过滤、
* 最近发送历史、通道/搜索/原始帧表单。
*
* 依赖:dbc.js(state/el/parseDbc/rebuildMergedDbc)
*/
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js';
var LS_KEY = 'dcu_can_session_v1';
var MAX_HISTORY = 200;
var SAVE_DEBOUNCE_MS = 350;
var saveTimer = null;
var restoring = false;
function safeJsonParse(raw) {
try {
return JSON.parse(raw);
} catch (_) {
return null;
}
}
function transmitterFilterToPlain() {
var out = {};
for (var k in state.transmitterFilter) {
if (state.transmitterFilter[k]) out[k] = true;
}
return out;
}
function collectUiFields() {
return {
defaultChannel: el.defaultChannel ? el.defaultChannel.value : '0',
manualChannel: el.manualChannel ? el.manualChannel.value : '0',
manualId: el.manualId ? el.manualId.value : '',
manualDlc: el.manualDlc ? el.manualDlc.value : '8',
manualData: el.manualData ? el.manualData.value : '',
dbcSearch: el.dbcSearch ? el.dbcSearch.value : ''
};
}
function applyUiFields(ui) {
if (!ui) return;
if (el.defaultChannel && ui.defaultChannel != null) {
el.defaultChannel.value = String(ui.defaultChannel);
}
if (el.manualChannel && ui.manualChannel != null) {
el.manualChannel.value = String(ui.manualChannel);
} else if (el.manualChannel && el.defaultChannel) {
el.manualChannel.value = el.defaultChannel.value;
}
if (el.manualId && ui.manualId != null) el.manualId.value = String(ui.manualId);
if (el.manualDlc && ui.manualDlc != null) el.manualDlc.value = String(ui.manualDlc);
if (el.manualData && ui.manualData != null) el.manualData.value = String(ui.manualData);
if (el.dbcSearch && ui.dbcSearch != null) el.dbcSearch.value = String(ui.dbcSearch);
if (el.canChannelPill && el.defaultChannel) {
el.canChannelPill.textContent = 'CH' + el.defaultChannel.value;
}
}
function buildPayload(options) {
options = options || {};
var sources = (state.dbcSources || []).map(function (s) {
return {
id: s.id,
name: s.name,
size: s.size || 0,
text: s.text || ''
};
}).filter(function (s) { return s.text; });
var history = Array.isArray(state.history) ? state.history.slice(0, MAX_HISTORY) : [];
if (options.historyLimit != null) {
history = history.slice(0, options.historyLimit);
}
return {
v: 1,
ts: Date.now(),
dbcSources: sources,
selectedMessageKey: state.selectedMessageKey || '',
signalValues: state.signalValues || {},
checkedKeys: state.checkedKeys || {},
expandedKeys: state.expandedKeys || {},
transmitterFilter: transmitterFilterToPlain(),
history: history,
ui: collectUiFields()
};
}
function writePayload(payload) {
localStorage.setItem(LS_KEY, JSON.stringify(payload));
}
/** @description 立即写入 localStorage(空间不足时逐步丢弃历史/信号)。 */
export function saveCanSessionNow() {
if (restoring) return;
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
try {
writePayload(buildPayload());
return;
} catch (e1) {
/* quota:先砍历史 */
}
try {
writePayload(buildPayload({ historyLimit: 40 }));
return;
} catch (e2) {
/* 再砍信号值,只保 DBC + 勾选 */
}
try {
var slim = buildPayload({ historyLimit: 0 });
slim.signalValues = {};
slim.history = [];
writePayload(slim);
} catch (e3) {
/* 放弃 */
}
}
/** @description 防抖保存(编辑信号/勾选等高频操作)。 */
export function scheduleCanPersist() {
if (restoring) return;
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
saveTimer = null;
saveCanSessionNow();
}, SAVE_DEBOUNCE_MS);
}
/**
* @description 从 localStorage 恢复会话到 state + 表单。
* @returns {{ restored: boolean, dbcCount: number, historyCount: number }}
*/
export function loadCanSession() {
var empty = { restored: false, dbcCount: 0, historyCount: 0 };
var raw = null;
try {
raw = localStorage.getItem(LS_KEY);
} catch (_) {
return empty;
}
if (!raw) return empty;
var data = safeJsonParse(raw);
if (!data || data.v !== 1) return empty;
restoring = true;
try {
state.dbcSources = [];
var list = Array.isArray(data.dbcSources) ? data.dbcSources : [];
for (var i = 0; i < list.length; i++) {
var src = list[i];
if (!src || !src.text) continue;
try {
var dbc = parseDbc(src.text);
if (!dbc.messages || !dbc.messages.length) continue;
state.dbcSources.push({
id: src.id || ('restored_' + i),
name: src.name || ('saved_' + (i + 1) + '.dbc'),
size: src.size || src.text.length,
text: src.text,
dbc: dbc
});
} catch (_) {
/* 单文件损坏跳过 */
}
}
rebuildMergedDbc();
state.signalValues = (data.signalValues && typeof data.signalValues === 'object')
? data.signalValues : {};
state.checkedKeys = (data.checkedKeys && typeof data.checkedKeys === 'object')
? data.checkedKeys : {};
state.expandedKeys = (data.expandedKeys && typeof data.expandedKeys === 'object')
? data.expandedKeys : {};
state.selectedMessageKey = data.selectedMessageKey || '';
state.history = Array.isArray(data.history) ? data.history.slice(0, MAX_HISTORY) : [];
state.transmitterFilter = Object.create(null);
if (data.transmitterFilter && typeof data.transmitterFilter === 'object') {
Object.keys(data.transmitterFilter).forEach(function (k) {
state.transmitterFilter[k] = !!data.transmitterFilter[k];
});
}
var anyTx = false;
for (var tk in state.transmitterFilter) {
if (state.transmitterFilter[tk]) { anyTx = true; break; }
}
state.transmitterFilterEmpty = !anyTx;
/* 选中键若不存在则清空,交由调用方选第一条 */
if (state.selectedMessageKey &&
(!state.dbc || !state.dbc.messageMap[state.selectedMessageKey])) {
state.selectedMessageKey = '';
}
applyUiFields(data.ui);
return {
restored: true,
dbcCount: state.dbcSources.length,
historyCount: state.history.length
};
} finally {
restoring = false;
}
}
/** @description 绑定 beforeunload / 可见性,降低丢失。 */
export function bindCanPersistLifecycle() {
window.addEventListener('beforeunload', function () {
saveCanSessionNow();
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') saveCanSessionNow();
});
}