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:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 列表。 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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 };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user