docs(frontend): 添加前端架构约定与回归清单文档

添加了完整的前端架构约定文档 (frontend_architecture.md),
包含以下主要内容:

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

同时移除了build相关的临时文件
This commit is contained in:
小龙虾
2026-07-27 01:50:48 +08:00
parent 2138a572ee
commit 33b41fd50a
19 changed files with 3555 additions and 3927 deletions
+49
View File
@@ -212,6 +212,55 @@ window.applyRebootingRuntimeState = applyRebootingRuntimeState;
window.applyUploadingRuntimeState = applyUploadingRuntimeState;
window.applyDeviceBusyRuntimeState = applyDeviceBusyRuntimeState;
// ==================== data-action 事件委托(替代 HTML 内联 onclick) ====================
// 处理函数分散在 app.js / ota/console-mode.js / ble_ota.js 三个入口,均挂载在
// window 上;委托在点击时才解引用,天然规避模块加载顺序问题。
document.addEventListener('click', function (ev) {
var target = ev.target.closest('[data-action]');
if (!target) return;
var arg = target.getAttribute('data-arg');
switch (target.getAttribute('data-action')) {
case 'set-console-mode': if (window.setConsoleMode) window.setConsoleMode(arg); break;
case 'test-connection': testConnection(); break;
case 'select-flash-target': if (window.selectFlashTarget) window.selectFlashTarget(arg); break;
case 'pick-shared-firmware': if (window.pickSharedFirmware) window.pickSharedFirmware(arg); break;
case 'clear-shared-firmware': if (window.clearSharedFirmware) window.clearSharedFirmware(arg); break;
case 'ble-pick-file': var fi = document.getElementById('firmwareFile'); if (fi) fi.click(); break;
case 'ble-clear-file': if (window.bleOta) window.bleOta.clearFile(); break;
case 'start-upload': startUpload(arg); break;
case 'restart-esp32': restartEsp32(); break;
case 'refresh-system-status': refreshSystemStatusNow(ev); break;
}
});
// 共享固件文件选择(原内联 onchange="handleSharedFileSelect(event, chip)")
['esp32', 'gd32'].forEach(function (chip) {
var input = document.getElementById(chip === 'esp32' ? 'fileInputEsp32' : 'fileInputGd32');
if (input) {
input.addEventListener('change', function (ev) {
if (window.handleSharedFileSelect) window.handleSharedFileSelect(ev, chip);
});
}
});
// BLE 上传区拖拽(原内联 ondragover/ondragleave/ondrop)
var bleZone = document.getElementById('uploadZone');
if (bleZone) {
bleZone.addEventListener('dragover', function (ev) {
ev.preventDefault();
bleZone.classList.add('dragover');
});
bleZone.addEventListener('dragleave', function () {
bleZone.classList.remove('dragover');
});
bleZone.addEventListener('drop', function (ev) {
ev.preventDefault();
bleZone.classList.remove('dragover');
var f = ev.dataTransfer.files[0];
if (f && window.bleOta) window.bleOta.notifyFileChange(f, null);
});
}
// ==================== 顶层副作用(原 app.js 末尾的初始化) ====================
setupDropZone('uploadZoneEsp32', 'esp32');
setupDropZone('uploadZoneGd32', 'gd32');
+10 -9
View File
@@ -16,11 +16,12 @@
* (操作不同的 DOM 元素集),此处保留原 can.js 版本以保证行为一致。
*/
/* 子模块 import 带同一 ?v=,避免浏览器缓存旧 dbc.js(无 getDeviceIp)且保证单例 state */
/* JS 间 import 一律不带 ?v=,版本戳只出现在 HTML 入口标签上,
* 避免同一模块因 query 不同被浏览器实例化两次(state/toast 单例分裂) */
import {
state, el, getBaseUrl, getDeviceIp, setDeviceIp, parseDbc, readDbcText, buildSummary,
rebuildMergedDbc, makeDbcSourceId
} from './can/dbc.js?v=20260717x';
} from './can/dbc.js';
import {
updateDbcFileInfo,
filterMessages,
@@ -30,11 +31,11 @@ import {
recomputeTransmitterFilterEmpty,
updateNodeFilterLabel,
updateBatchUi
} from './can/messages.js?v=20260717x';
} from './can/messages.js';
import {
ensureMessageDefaults,
updateEncodedPreview
} from './can/signals.js?v=20260717x';
} from './can/signals.js';
import {
renderHistory,
sendSelectedMessage,
@@ -43,15 +44,15 @@ import {
sendManualFrame,
resetManualFields,
loadPreviewToManual
} from './can/send.js?v=20260720b';
} from './can/send.js';
import {
scheduleCanPersist,
loadCanSession,
bindCanPersistLifecycle
} from './can/persist.js?v=20260717x';
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js?v=20260717x';
import { showToast } from './common/toast.js?v=20260717x';
import { initCanMonitor } from './can_monitor.js?v=20260719s';
} from './can/persist.js';
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js';
import { showToast } from './common/toast.js';
import { initCanMonitor } from './can_monitor.js';
// ==================== 本地工具 ====================
// 原 can.js 的 formatUptime 输出格式与 common/utils.js 略有差异(d>0 时只到 h),
+8 -8
View File
@@ -6,6 +6,9 @@
* 本模块是 can 子系统的基础层,不依赖其他 can 子模块;state/el 在此导出,
* 供 messages/signals/send 共享(ES Module 的 live binding 保证引用一致)。
*/
import {
DEFAULT_ESP_IP, getSavedDeviceIp, saveDeviceIp
} from '../common/base-url.js';
// ==================== 共享状态 ====================
export var state = {
@@ -31,15 +34,12 @@ export var state = {
export var el = {};
// ==================== 基础工具 ====================
var LS_ESP_IP = 'dcu_esp_ip';
var DEFAULT_ESP_IP = '192.168.31.127';
// IP 存取通用实现在 common/base-url.js,此处包装 CAN 页特有的 el.toolIp 回退。
/** @description 设备 IP:优先 localStorage(与 OTA 页共用),否则默认。 */
/** @description 设备 IP:优先 localStorage(与 OTA 页共用),其次页面输入框,否则默认。 */
export function getDeviceIp() {
try {
var saved = localStorage.getItem(LS_ESP_IP);
if (saved && saved.trim()) return saved.trim();
} catch (_) {}
var saved = getSavedDeviceIp();
if (saved) return saved;
if (el.toolIp && el.toolIp.value && el.toolIp.value.trim()) {
return el.toolIp.value.trim();
}
@@ -50,7 +50,7 @@ export function getDeviceIp() {
export function setDeviceIp(ip) {
if (!ip || !String(ip).trim()) return;
var v = String(ip).trim();
try { localStorage.setItem(LS_ESP_IP, v); } catch (_) {}
saveDeviceIp(v);
if (el.toolIp) el.toolIp.value = v;
}
+4 -4
View File
@@ -7,10 +7,10 @@
* common/utils.js(escapeHtml/formatBytes)、
* signals.js(renderInlineSignalGrid/ensureMessageDefaults/updateEncodedPreview)。
*/
import { state, el, padHex, normalizeWhitespace, cssEscape, findMessageByKey } from './dbc.js?v=20260717x';
import { escapeHtml, formatBytes } from '../common/utils.js?v=20260717x';
import { renderInlineSignalGrid, ensureMessageDefaults, updateEncodedPreview } from './signals.js?v=20260717x';
import { scheduleCanPersist } from './persist.js?v=20260717x';
import { state, el, padHex, normalizeWhitespace, cssEscape, findMessageByKey } from './dbc.js';
import { escapeHtml, formatBytes } from '../common/utils.js';
import { renderInlineSignalGrid, ensureMessageDefaults, updateEncodedPreview } from './signals.js';
import { scheduleCanPersist } from './persist.js';
// ==================== DBC 文件信息 ====================
/** @description 更新 DBC 文件信息卡片(文件名/报文数/信号数/节点数/大小),并刷新多 DBC 摘要条与文件 chip 列表。 */
+1 -1
View File
@@ -6,7 +6,7 @@
*
* 依赖:dbc.js(state/el/parseDbc/rebuildMergedDbc)
*/
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js?v=20260717x';
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js';
var LS_KEY = 'dcu_can_session_v1';
var MAX_HISTORY = 200;
+6 -6
View File
@@ -12,12 +12,12 @@
* 注意:formatTime 保留本地实现(HH:MM:SS,不含毫秒),与 common/utils.js
* 的 formatTime(含毫秒)不同,以保证发送历史时间戳输出格式一致。
*/
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js?v=20260717x';
import { escapeHtml } from '../common/utils.js?v=20260717x';
import { showToast } from '../common/toast.js?v=20260717x';
import { updateBatchUi } from './messages.js?v=20260717x';
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js?v=20260717x';
import { scheduleCanPersist } from './persist.js?v=20260717x';
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js';
import { escapeHtml } from '../common/utils.js';
import { showToast } from '../common/toast.js';
import { updateBatchUi } from './messages.js';
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js';
import { scheduleCanPersist } from './persist.js';
// ==================== 本地工具(保持与原 can.js 一致) ====================
function formatTime(date) {
+3 -3
View File
@@ -5,9 +5,9 @@
*
* 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。
*/
import { state, el, padHex, bytesToHex, cssEscape, findSignal, findMessageByKey } from './dbc.js?v=20260717x';
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717x';
import { scheduleCanPersist } from './persist.js?v=20260717x';
import { state, el, padHex, bytesToHex, cssEscape, findSignal, findMessageByKey } from './dbc.js';
import { escapeHtml as sanitizeHtml } from '../common/utils.js';
import { scheduleCanPersist } from './persist.js';
// ==================== 信号值存取 ====================
/** @description 确保报文有 signalValues bag,并为每个信号填充默认值。返回 bag。 */
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* can_monitor.js - CAN 接收监控(独立页 can_monitor.html 与 can.html 右侧抽屉共用)
*/
import { showToast } from './common/toast.js?v=20260717x';
import { showToast } from './common/toast.js';
import { escapeHtml } from './common/utils.js';
var state = { fbTimer: null, fbInFlight: false };
+34
View File
@@ -0,0 +1,34 @@
/**
* base-url.js - 设备 IP / 基础 URL 公共助手
*
* 各页面共用 localStorage key dcu_esp_ip(OTA / CAN / 电机页一致)。
* 本模块只提供纯 localStorage + 默认值的通用实现;页面特有的输入框回退
* (如 CAN 页 el.toolIp、OTA 页 #espIp)由各自入口在此基础上包装。
*/
export var LS_ESP_IP = 'dcu_esp_ip';
export var DEFAULT_ESP_IP = '192.168.31.127';
/** @description 读取 localStorage 中保存的设备 IP,无有效值返回空串。 */
export function getSavedDeviceIp() {
try {
var saved = localStorage.getItem(LS_ESP_IP);
if (saved && saved.trim()) return saved.trim();
} catch (_) {}
return '';
}
/** @description 保存设备 IP 到 localStorage(空值忽略)。 */
export function saveDeviceIp(ip) {
if (!ip || !String(ip).trim()) return;
try { localStorage.setItem(LS_ESP_IP, String(ip).trim()); } catch (_) {}
}
/** @description 设备 IP:localStorage 优先,否则默认值。 */
export function getDeviceIp() {
return getSavedDeviceIp() || DEFAULT_ESP_IP;
}
/** @description 拼接设备基础 URL(http://<ip>)。 */
export function getBaseUrl() {
return 'http://' + getDeviceIp();
}
File diff suppressed because it is too large Load Diff