/** * @file ui.js * @brief 通用 UI 组件 — Toast, Modal, Table, Icon, Utilities */ /* ================================================================== * SVG 内联图标 (Lucide 风格, 18×18) * ================================================================== */ var Icon = { _map: { monitor: '', dashboard: '', signal: '', link: '', zap: '', sliders: '', settings: '', database: '', bug: '', cpu: '', activity: '', terminal: '', check: '', plus: '', trash: '', edit: '', alert: '', }, get: function(name, cls) { return (this._map[name] || '').replace('class="icon-svg"', 'class="' + (cls || 'icon-svg') + '"'); } }; /* ================================================================== * Toast 通知 * ================================================================== */ function toast(msg, type) { type = type || 'info'; var el = document.createElement('div'); el.className = 'toast toast-' + type; el.textContent = msg; document.getElementById('toast-container').appendChild(el); setTimeout(function() { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; }, 2500); setTimeout(function() { el.remove(); }, 2800); } /* ================================================================== * Modal 模态框 * ================================================================== */ function showModal(title, bodyHtml, onOk, okText) { okText = okText || I18n.t('toast.confirm'); var overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); overlay.querySelector('.btn-cancel').onclick = function() { overlay.remove(); }; overlay.querySelector('.btn-ok').onclick = function() { onOk(overlay); }; overlay.addEventListener('click', function(e) { if (e.target === overlay) { overlay.remove(); } }); return overlay; } function getModalVal(overlay, selector) { var el = overlay.querySelector(selector); return el ? el.value : ''; } /* ================================================================== * Table 渲染工具 * ================================================================== */ function esc(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function fmtVal(v) { return (v == null || v === '') ? '\u2014' : esc(String(v)); } function td(s, cls) { return '' + s + ''; } function th(s) { return '' + s + ''; } /** * @brief 渲染信号管理页面通用模板 */ function renderSignalPage(type, showCheckbox, extraHeader, extraCell) { var signals = SignalStore.get(type); var rows = ''; var colCount = (showCheckbox ? 1 : 0) + 5 + (extraHeader ? 1 : 0) + 1; if (signals.length === 0) { rows = '' + I18n.t('signal.no_data') + ''; } else { signals.forEach(function(s, idx) { var xtra = extraCell ? extraCell(s) : ''; var acts = (type === 'out' || type === 'yk' || type === 'ao') ? '' + '' + '' : ''; rows += '' + (showCheckbox ? '' : '') + '' + (idx + 1) + '' + '' + esc(s.saddr) + '' + '' + esc(s.desc || '') + '' + '' + esc(s.type || '?') + '' + '' + fmtVal(s.val) + '' + xtra + '' + acts + '' + ''; }); } var headerHtml = ''; if (showCheckbox) headerHtml += ''; headerHtml += '#'; headerHtml += '' + I18n.t('signal.saddr') + ''; headerHtml += '' + I18n.t('signal.desc') + ''; headerHtml += '' + I18n.t('signal.type') + ''; headerHtml += '' + I18n.t('signal.val') + ''; if (extraHeader) headerHtml += '' + extraHeader + ''; headerHtml += '' + I18n.t('signal.action') + ''; return '
' + '
' + type.toUpperCase() + ' — ' + signals.length + ' signals' + '
' + '' + '' + '
' + '
' + '' + headerHtml + '' + '' + rows + '' + '
'; } /* ================================================================== * 信号 CRUD 操作 * ================================================================== */ var SignalActions = { add: function(type) { var body = ''; body += '
'; body += '
'; body += '
' + '
'; if (type === 'yk' || type === 'ao' || type === 'param') { body += '
' + '
'; } showModal(I18n.t('signal.add_title') + ' (' + type.toUpperCase() + ')', body, function(ov) { var saddr = getModalVal(ov, '#mf-saddr'); if (!saddr) { toast(I18n.t('toast.saddr_required'), 'error'); return; } var msg = { saddr: saddr, signal_type: type, curd: 'add', signal_data: '', setting_zone: '0' }; WsClient.send(msg); toast(I18n.t('toast.add_sent') + ': ' + saddr, 'success'); ov.remove(); }); }, del: function(type, saddr) { showModal(I18n.t('signal.confirm_del'), '

' + I18n.t('signal.confirm_del_msg') + ' ' + esc(saddr) + '?

', function(ov) { WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); SignalStore.remove(type, saddr); toast(I18n.t('toast.del_sent') + ': ' + saddr, 'info'); ov.remove(); }, Icon.get('trash') + ' ' + I18n.t('signal.delete')); }, batchDel: function(type) { var checked = [].slice.call(document.querySelectorAll('#tbody-' + type + ' input[type=checkbox]:checked')); if (checked.length === 0) { toast(I18n.t('toast.no_selected'), 'error'); return; } showModal(I18n.t('signal.confirm_batch_del'), '

' + I18n.t('signal.confirm_batch_msg') + ' ' + checked.length + ' ' + I18n.t('signal.confirm_batch_del') + '?

', function(ov) { checked.forEach(function(cb) { var tr = cb.closest('tr'); var saddr = tr ? tr.getAttribute('data-saddr') : ''; if (saddr) { WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); SignalStore.remove(type, saddr); } }); toast(checked.length + ' ' + I18n.t('toast.batch_del_sent'), 'info'); ov.remove(); }, Icon.get('trash') + ' ' + I18n.t('signal.delete') + ' ' + checked.length); }, set: function(type, saddr) { showModal(I18n.t('signal.set_title') + ' \u2014 ' + esc(saddr), '
', function(ov) { var val = getModalVal(ov, '#mf-val'); WsClient.send({ saddr: saddr, signal_type: type, curd: 'set', signal_data: val, setting_zone: '0' }); toast(I18n.t('toast.set_sent') + ': ' + saddr + ' = ' + val, 'success'); ov.remove(); }, I18n.t('signal.set')); } }; /* ---- Event delegation: data-action buttons ---- */ document.addEventListener('click', function(e) { var btn = e.target.closest('[data-action]'); if (!btn) return; var action = btn.getAttribute('data-action'); var saddr = btn.getAttribute('data-saddr'); var type = btn.getAttribute('data-page-type'); if (!type) { var tbody = e.target.closest('tbody[data-type]'); if (tbody) type = tbody.getAttribute('data-type'); } if (!type) return; if (action === 'set-go') { e.preventDefault(); var row = btn.closest('tr'); var inp = row ? row.querySelector('.sig-set-input') : null; var val = inp ? inp.value.trim() : ''; if (!val) { toast(I18n.t('toast.val_required'), 'error'); return; } WsClient.send({ saddr: saddr, signal_type: type, curd: 'set', signal_data: val, setting_zone: '0' }); /* 立即更新本地 SignalStore 和 DOM,推送到达前先显示 */ var arr = SignalStore.get(type); for (var k = 0; k < arr.length; k++) { if (arr[k].saddr === saddr) { arr[k].val = val; break; } } /* 直接更新当前行的值单元格 */ var vc = row.querySelector('.cell-val'); if (vc) { vc.textContent = val; vc.classList.add('changed'); setTimeout(function(){vc.classList.remove('changed');},600); } inp.value = ''; } else if (action === 'del') { e.preventDefault(); SignalActions.del(type, saddr); } }); /* Enter key on inline input triggers set */ document.addEventListener('keydown', function(e) { if (e.key === 'Enter' && e.target.classList.contains('sig-set-input')) { e.preventDefault(); var inp = e.target; var row = inp.closest('tr'); var btn = row ? row.querySelector('[data-action="set-go"]') : null; if (btn) btn.click(); } });