/** * @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, headers, extraCols, actionsForRow) { var signals = SignalStore.get(type); var rows = ''; if (signals.length === 0) { rows = '' + I18n.t('signal.no_data') + ''; } else { signals.forEach(function(s) { var xtra = extraCols ? extraCols(s) : ''; var acts = actionsForRow ? actionsForRow(s) : ''; rows += '' + '' + esc(s.saddr) + '' + '' + esc(s.desc || '') + '' + xtra + '' + fmtVal(s.val) + '' + '' + acts + '' + ''; }); } return '
' + '
' + type.toUpperCase() + ' \u2014 ' + signals.length + ' signals' + '
' + '' + '' + '
' + '
' + '' + headers.map(function(h) { return th(h); }).join('') + '' + '' + rows + '' + '
' + I18n.t('signal.val') + '' + I18n.t('signal.action') + '
'; } /* ================================================================== * 信号 CRUD 操作 * ================================================================== */ var SignalActions = { add: function(type) { var body = ''; body += '
'; body += '
'; if (type === 'in') { body += '
'; } else { 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' }; if (type === 'in') { msg.link_saddr = getModalVal(ov, '#mf-link'); } 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); Router.dispatch(Router.currentHash); 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 saddr = cb.closest('tr').dataset.saddr; WsClient.send({ saddr: saddr, signal_type: type, curd: 'del', setting_zone: '0' }); }); 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')); } };