Files
DCU_DesktopController/js/common/utils.js
T
cbb eac3717f9a feat: 添加局域网设备发现功能和链路胶囊优化
- 实现局域网设备发现面板,支持探测ESP32中继板并自动切换端点
- 添加POST /api/lan/probe接口用于设备探测,返回三态判决结果
- 实现快扫和全段扫描两种模式,支持种子地址优先探测
- 添加端点唯一写手机制,防止多处同时修改设备IP
- 优化链路胶囊UI显示WiFi/EtherCAT/BLE连接状态和设备名称
- 修复EtherCAT链路OTA刷写期间PDO循环暂停和AL状态处理问题
- 调整PDO周期设置范围为0.125-4ms,支持亚毫秒显示为微秒
- 添加ESP32 WiFi使能开关控件样式和数据来源角标显示
2026-10-13 00:48:34 +08:00

141 lines
5.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.
/**
* utils.js - 通用工具函数
*
* 格式化 / 转义工具函数,供各子模块和 can.js / can_monitor.js 共享。
*/
// ==================== 工具函数 ====================
/** @description 格式化字节大小为人类可读字符串(B/KB/MB)。 */
/** @description 字节格式化为 B / KB(不升到 MB,避免 1024KB 显示成 1.00MB)。 */
export function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes < 0) return '--';
if (bytes < 1024) return bytes + ' B';
var kb = bytes / 1024;
if (Math.abs(kb - Math.round(kb)) < 1e-9) return Math.round(kb) + ' KB';
return kb.toFixed(1) + ' KB';
}
/** @description 格式化 Date 对象为 HH:MM:SS.mmm 时间字符串。 */
export function formatTime(date) {
var h = String(date.getHours()).padStart(2, '0');
var m = String(date.getMinutes()).padStart(2, '0');
var s = String(date.getSeconds()).padStart(2, '0');
var ms = String(date.getMilliseconds()).padStart(3, '0');
return h + ':' + m + ':' + s + '.' + ms;
}
/** @description 格式化秒数为人类可读运行时间(d/h/m/s)。 */
export function formatUptime(sec) {
if (!Number.isFinite(sec) || sec < 0) return '--';
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor(sec % 60);
if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
/** @description 转义 HTML 特殊字符,防止 XSS 注入。 */
export function escapeHtml(text) {
return String(text == null ? '' : text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* @description 将字节数组(Uint8Array 或 ArrayBuffer)转为小写 hex 字符串。
* SHA-256 等哈希显示前需要 toUpperCase()。
* @param {Uint8Array|ArrayBuffer} bytes
* @returns {string}
*/
export function bytesToHex(bytes) {
var arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
var out = '';
for (var i = 0; i < arr.length; i++) {
var h = arr[i].toString(16);
if (h.length < 2) h = '0' + h;
out += h;
}
return out;
}
/**
* @description 当前是否有刷写/OTA 在跑(跨 iframe 共享)。
* 多 iframe 保活外壳下各功能页是**独立文档**,window 变量不通用(刷写页的
* window.bootEcatBusy 在 CAN 页里读不到),所以走 localStorage 这一条共享通道:
* 刷写方(js/ota/boot_ecat.js)写 dcu_flash_busy_until = 预计结束时刻(ms),
* 轮询方据此避让。带到期时间,页面中途关掉/异常也不会把标记永久挂住。
* @returns {boolean} 刷写窗口内为 true
*/
export function flashBusyNow() {
try {
var until = parseInt(localStorage.getItem('dcu_flash_busy_until'), 10);
return Number.isFinite(until) && until > Date.now();
} catch (_) {
return false;
}
}
/** @description 写前比较的 textContent 更新:值未变则不碰 DOM。 */
export function setTextIfChanged(el, txt) {
if (el && el.textContent !== txt) el.textContent = txt;
}
/** @description 写前比较的 className 更新。 */
export function setClassIfChanged(el, cls) {
if (el && el.className !== cls) el.className = cls;
}
/** @description 写前比较的 style[prop] 更新。 */
export function setStyleIfChanged(el, prop, v) {
if (el && el.style && el.style[prop] !== v) el.style[prop] = v;
}
/** @description classList.toggle 前比较,避免无谓样式失效。 */
export function toggleClsIfChanged(el, cls, on) {
if (!el) return;
var has = el.classList.contains(cls);
if (has !== !!on) el.classList.toggle(cls, !!on);
}
/**
* @description 无设备静默态:给 <html> 挂 data-dcu-calm,由 responsive.css 掐动画。
* 全仓唯一实现(connection.js / motor main 均 import 此处)。
*/
export function setQuietMode(quiet) {
var v = quiet ? '1' : '0';
if (document.documentElement.dataset.dcuCalm !== v) {
document.documentElement.dataset.dcuCalm = v;
}
}
/**
* @description 更新数据格右上角来源角标(WiFi / ECAT / BLE)。
* 双通路无感切换:哪条链路**最后写入**本格,角标就标哪条(last-writer-wins)。
* 同格若两路都在写,后到的会盖掉先到的来源——切换瞬间角标跟着变是预期行为。
* @param {string|Element} cell 带 data-cell 的格子,或格子内任意元素
* @param {'wifi'|'ecat'|'ble'|'off'} src 来源
*/
export function setCellSource(cell, src) {
var root = cell;
if (typeof cell === 'string') {
root = document.querySelector('[data-cell="' + cell + '"]');
} else if (cell && cell.nodeType === 1 && !cell.classList.contains('src-badge')) {
root = typeof cell.closest === 'function' ? cell.closest('[data-cell]') : null;
}
if (!root || !root.querySelector) return;
/* 不用 :scope —— 部分环境对它支持不稳,角标是格子内唯一直属 .src-badge */
var badges = root.getElementsByClassName('src-badge');
var badge = badges && badges[0];
if (!badge) return;
/* dual = WiFi 与 ECAT 同时在线(双通路) */
var map = {
wifi: 'WiFi', ecat: 'ECAT', ble: 'BLE', off: '--',
dual: 'WiFi·ECAT',
};
var label = map[src] || '--';
var kind = map[src] ? src : 'off';
/* dual 用 wifi 同色系加粗边,避免第三套色 */
var cls = 'src-badge src-' + (kind === 'dual' ? 'dual' : kind);
if (badge.className !== cls) badge.className = cls;
if (badge.textContent !== label) badge.textContent = label;
if (root.dataset.src !== kind) root.dataset.src = kind;
}