RTU_ALL_AI/config/web/js/ui.js

273 lines
15 KiB
JavaScript
Raw Permalink 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.

/**
* @file ui.js
* @brief 通用 UI 组件 — Toast, Modal, Table, Icon, Utilities
*/
/* ==================================================================
* SVG 内联图标 (Lucide 风格, 18×18)
* ================================================================== */
var Icon = {
_map: {
monitor: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
dashboard: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
signal: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>',
link: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
zap: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
sliders: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>',
settings: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>',
database: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
bug: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="12" rx="3"/><path d="M8 5V3h8v2M12 12v4M8 12h.01M16 12h.01"/></svg>',
cpu: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg>',
activity: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
terminal: '<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
check: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>',
plus: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
trash: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
edit: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
alert: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ff4757" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>',
},
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 = '<div class="modal-box">'
+ '<div class="modal-title">' + title + '</div>'
+ '<div class="modal-body">' + bodyHtml + '</div>'
+ '<div class="modal-actions">'
+ '<button class="btn btn-cancel">' + I18n.t('toast.cancel') + '</button>'
+ '<button class="btn btn-primary btn-ok">' + okText + '</button>'
+ '</div></div>';
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function fmtVal(v) {
return (v == null || v === '') ? '\u2014' : esc(String(v));
}
function td(s, cls) {
return '<td class="' + (cls || '') + '">' + s + '</td>';
}
function th(s) {
return '<th>' + s + '</th>';
}
/**
* @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 = '<tr class="empty-row"><td colspan="' + colCount + '">' + I18n.t('signal.no_data') + '</td></tr>';
}
else
{
signals.forEach(function(s, idx) {
var xtra = extraCell ? extraCell(s) : '';
var acts = (type === 'out' || type === 'yk' || type === 'ao')
? '<input class="sig-set-input" data-action="set-inline" data-saddr="' + esc(s.saddr) + '" placeholder="value" style="width:55px;padding:2px 4px;font-size:12px;border:1px solid var(--border);border-radius:3px;background:var(--bg-input);color:var(--text-primary);font-family:var(--font-mono)">'
+ '<button class="btn btn-sm" data-action="set-go" data-saddr="' + esc(s.saddr) + '" style="padding:2px 6px;font-size:12px">' + I18n.t('signal.set') + '</button>'
+ '<button class="btn btn-danger btn-sm" data-action="del" data-saddr="' + esc(s.saddr) + '">' + Icon.get('trash') + '</button>'
: '<button class="btn btn-danger btn-sm" data-action="del" data-saddr="' + esc(s.saddr) + '">' + Icon.get('trash') + '</button>';
rows += '<tr data-saddr="' + esc(s.saddr) + '">'
+ (showCheckbox ? '<td><input type="checkbox"></td>' : '')
+ '<td class="cell-mono" style="color:var(--text-muted)">' + (idx + 1) + '</td>'
+ '<td class="cell-mono">' + esc(s.saddr) + '</td>'
+ '<td class="cell-desc">' + esc(s.desc || '') + '</td>'
+ '<td class="cell-mono">' + esc(s.type || '?') + '</td>'
+ '<td class="cell-val">' + fmtVal(s.val) + '</td>'
+ xtra
+ '<td class="cell-acts" data-saddr="' + esc(s.saddr) + '">' + acts + '</td>'
+ '</tr>';
});
}
var headerHtml = '';
if (showCheckbox) headerHtml += '<th style="width:36px"><input type="checkbox" id="cb-all-' + type + '" onclick="var v=this.checked;document.querySelectorAll(\'#tbody-' + type + ' input[type=checkbox]\').forEach(function(c){c.checked=v;})"></th>';
headerHtml += '<th style="width:36px">#</th>';
headerHtml += '<th>' + I18n.t('signal.saddr') + '</th>';
headerHtml += '<th>' + I18n.t('signal.desc') + '</th>';
headerHtml += '<th>' + I18n.t('signal.type') + '</th>';
headerHtml += '<th>' + I18n.t('signal.val') + '</th>';
if (extraHeader) headerHtml += '<th>' + extraHeader + '</th>';
headerHtml += '<th>' + I18n.t('signal.action') + '</th>';
return '<div class="card">'
+ '<div class="card-header"><span>' + type.toUpperCase() + ' — ' + signals.length + ' signals</span>'
+ '<div class="btn-group">'
+ '<button class="btn btn-primary btn-sm" onclick="SignalActions.add(\'' + type + '\')">' + Icon.get('plus') + ' ' + I18n.t('signal.add') + '</button>'
+ '<button class="btn btn-danger btn-sm" onclick="SignalActions.batchDel(\'' + type + '\')">' + Icon.get('trash') + ' ' + I18n.t('signal.delete_sel') + '</button>'
+ '</div></div>'
+ '<div class="table-wrap"><table class="signal-table">'
+ '<thead><tr>' + headerHtml + '</tr></thead>'
+ '<tbody id="tbody-' + type + '" data-type="' + type + '">' + rows + '</tbody>'
+ '</table></div></div>';
}
/* ==================================================================
* 信号 CRUD 操作
* ================================================================== */
var SignalActions = {
add: function(type) {
var body = '';
body += '<div class="form-group"><label class="form-label">' + I18n.t('signal.saddr') + '</label><input class="form-input" id="mf-saddr" placeholder="e.g. plc.st.out.0"></div>';
body += '<div class="form-group"><label class="form-label">' + I18n.t('signal.desc') + '</label><input class="form-input" id="mf-desc" placeholder="Signal description"></div>';
body += '<div class="form-group"><label class="form-label">' + I18n.t('signal.type') + '</label>'
+ '<select class="form-select" id="mf-dtype">'
+ '<option value="U8">U8</option><option value="U16">U16</option>'
+ '<option value="U32">U32</option><option value="F32">F32</option>'
+ '<option value="STR">STR</option></select></div>';
if (type === 'yk' || type === 'ao' || type === 'param') {
body += '<div class="form-group"><label class="form-label">' + I18n.t('signal.ctrl') + '</label>'
+ '<select class="form-select" id="mf-ctrl">'
+ '<option value="0">' + I18n.t('signal.ctrl_direct') + '</option>'
+ '<option value="1">' + I18n.t('signal.ctrl_sbo') + '</option></select></div>';
}
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'),
'<p>' + I18n.t('signal.confirm_del_msg') + ' <strong>' + esc(saddr) + '</strong>?</p>',
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'),
'<p>' + I18n.t('signal.confirm_batch_msg') + ' ' + checked.length + ' ' + I18n.t('signal.confirm_batch_del') + '?</p>',
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),
'<div class="form-group"><label class="form-label">' + I18n.t('signal.new_val') + '</label><input class="form-input" id="mf-val" placeholder="Value"></div>',
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();
}
});