RTU_ALL_AI/config/web/js/ui.js

221 lines
12 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.

/**
* @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, headers, extraCols, actionsForRow) {
var signals = SignalStore.get(type);
var rows = '';
if (signals.length === 0)
{
rows = '<tr class="empty-row"><td colspan="' + headers.length + '">' + I18n.t('signal.no_data') + '</td></tr>';
}
else
{
signals.forEach(function(s) {
var xtra = extraCols ? extraCols(s) : '';
var acts = actionsForRow ? actionsForRow(s) : '';
rows += '<tr>'
+ '<td class="cell-mono">' + esc(s.saddr) + '</td>'
+ '<td class="cell-desc">' + esc(s.desc || '') + '</td>'
+ xtra
+ '<td class="cell-val">' + fmtVal(s.val) + '</td>'
+ '<td>' + acts + '</td>'
+ '</tr>';
});
}
return '<div class="card">'
+ '<div class="card-header"><span>' + type.toUpperCase() + ' \u2014 ' + 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>' + headers.map(function(h) { return th(h); }).join('') + '<th>' + I18n.t('signal.val') + '</th><th>' + I18n.t('signal.action') + '</th></tr></thead>'
+ '<tbody id="tbody-' + 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>';
if (type === 'in')
{
body += '<div class="form-group"><label class="form-label">' + I18n.t('signal.link') + '</label><input class="form-input" id="mf-link" placeholder="e.g. self_ptl.st.0"></div>';
}
else
{
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' };
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'),
'<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);
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'),
'<p>' + I18n.t('signal.confirm_batch_msg') + ' ' + checked.length + ' ' + I18n.t('signal.confirm_batch_del') + '?</p>',
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),
'<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'));
}
};