Категории

  • Техническая поддержка по системе IntraSCADA

    14 Темы
    44 Сообщения
    I
    Здравствуйте, на основе первой версии сделал версию с несколькими маркерами и печатью в файл. Прикрепляю исходный код REST API и виджета. Внешний вид: [image: 1788269451962-3d8bdb83-301c-4e4d-8139-9cb25c81ffaa-image.jpeg] REST API метод GET маршрут /forGraph module.exports = async (req, res, holder, debug) => { try { const q = req.query; const start = Number(q.start_ms) || (Date.now() - 3600000); const end = Number(q.end_ms) || Date.now(); const prop = q.prop || 'CV'; const tagsParam = q.tags; if (!tagsParam) throw { message: 'Не указан параметр tags (список устройств через запятую)' }; const tagsDecoded = decodeURIComponent(String(tagsParam)); const tags = tagsDecoded.split(',').map(t => t.trim()).filter(Boolean); //const tags = String(tagsParam).split(',').map(t => t.trim()).filter(Boolean); const data = {}; for (const tag of tags) { const curdev = holder.getDevice(tag); if (!curdev) { debug('Устройство не найдено: ' + tag); data[tag] = []; continue; } const records = await holder.dm.get('records', { start, end, dn_prop: curdev.dn + '.' + prop }); // Приводим записи к единому формату { ts, value } — // на случай если поля в БД называются иначе (time/val и т.п.) data[tag] = (records || []) .map(r => ({ ts: Number(r.ts !== undefined ? r.ts : (r.time !== undefined ? r.time : r.dt)), value: Number(r.value !== undefined ? r.value : (r.val !== undefined ? r.val : r.v)) })) .filter(p => isFinite(p.ts) && isFinite(p.value)) .sort((a, b) => a.ts - b.ts); } res.json({ res: 1, data }); debug(JSON.stringify({ res: 1, tags, start, end, prop, counts: Object.fromEntries(tags.map(t => [t, data[t].length])) })); } catch (e) { res.status(500).json({ res: 0, message: e.message }); debug('ERROR: ' + e.message); } }; HTML виджет <style> #root-${uuid} { --bg: #10161c; --panel: #161f28; --panel-2: #1c2732; --line: #2a3744; --text: #dbe4ec; --text-dim: #8ea0b0; --accent: #4fb3d9; --accent-2: #f2a154; --danger: #e2694b; --radius: 6px; font-family: "Segoe UI", Roboto, Arial, sans-serif; color: var(--text); background: var(--bg); width: 100%; height: 100%; box-sizing: border-box; padding: 14px; display: flex; flex-direction: column; gap: 12px; overflow: auto; } #root-${uuid} * { box-sizing: border-box; } #root-${uuid} .controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; } #root-${uuid} .field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-dim); } #root-${uuid} .field input { background: var(--panel-2); border: 1px solid var(--line); color: var(--text); border-radius: 4px; padding: 6px 8px; font-size: 13px; font-family: inherit; min-width: 170px; } #root-${uuid} .field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; } #root-${uuid} button { border: 1px solid var(--line); background: var(--accent); color: #071019; font-weight: 600; font-size: 13px; padding: 8px 16px; border-radius: 4px; cursor: pointer; transition: filter 0.15s ease; } #root-${uuid} button:hover { filter: brightness(1.08); } #root-${uuid} button:active { filter: brightness(0.95); } #root-${uuid} button.secondary { background: transparent; color: var(--text); border: 1px solid var(--line); } #root-${uuid} button:disabled { opacity: 0.5; cursor: not-allowed; } #root-${uuid} .status { font-size: 12px; color: var(--text-dim); min-width: 160px; } #root-${uuid} .status.error { color: var(--danger); } #root-${uuid} .chart-wrap { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; height: 360px; min-height: 260px; flex: 0 0 auto; } #root-${uuid} .chart-canvas-holder { position: relative; width: 100%; height: 100%; } #root-${uuid} canvas { width: 100% !important; height: 100% !important; display: block; } #root-${uuid} .empty-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--text-dim); font-size: 13px; text-align: center; padding: 20px; } #root-${uuid} .marker-line { position: absolute; top: 0; width: 0; border-left: 2px dashed; cursor: ew-resize; touch-action: none; z-index: 5; } #root-${uuid} .marker-handle { position: absolute; top: -22px; left: -11px; width: 22px; height: 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: #071019; cursor: ew-resize; touch-action: none; user-select: none; } #root-${uuid} table.marker-table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; font-size: 13px; } #root-${uuid} table.marker-table caption { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-dim); caption-side: top; } #root-${uuid} table.marker-table th, #root-${uuid} table.marker-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; } #root-${uuid} table.marker-table thead th { background: var(--panel-2); color: var(--text-dim); font-weight: 600; font-size: 12px; } #root-${uuid} table.marker-table tbody tr:last-child td { border-bottom: none; } #root-${uuid} .marker-dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: middle; } /* Печать теперь выполняется через отдельное чистое окно (см. openPrintWindow), поэтому здесь достаточно на всякий случай прятать панель управления, если кто-то всё же нажмёт Ctrl+P прямо на экране IntraSCADA. */ #root-${uuid} .no-print { /* переопределяется ниже через @media print */ } @media print { #root-${uuid} .no-print { display: none !important; } } </style> <div id="root-${uuid}"> <div class="controls no-print"> <div class="field"> <label for="dtStart-${uuid}">Начало эксперимента</label> <input type="text" id="dtStart-${uuid}" placeholder="ДД.ММ.ГГГГ ЧЧ:ММ:СС" pattern="\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}(:\d{2})?" title="Формат: ДД.ММ.ГГГГ ЧЧ:ММ:СС" /> </div> <div class="field"> <label for="dtEnd-${uuid}">Конец эксперимента</label> <input type="text" id="dtEnd-${uuid}" placeholder="ДД.ММ.ГГГГ ЧЧ:ММ:СС" pattern="\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}(:\d{2})?" title="Формат: ДД.ММ.ГГГГ ЧЧ:ММ:СС" /> </div> <button id="btnLoad-${uuid}" type="button">Показать</button> <button id="btnPrint-${uuid}" type="button" class="secondary" disabled>Печать</button> <span id="status-${uuid}" class="status"></span> </div> <div class="chart-wrap" id="chartWrap-${uuid}"> <div class="chart-canvas-holder" id="chartHolder-${uuid}"> <canvas id="canvas-${uuid}"></canvas> </div> <div class="empty-state" id="emptyState-${uuid}"> Укажите период эксперимента и нажмите «Показать» </div> </div> <table class="marker-table" id="table-${uuid}"> <caption>Значения температур в точках маркеров</caption> <thead> <tr id="tableHead-${uuid}"></tr> </thead> <tbody id="tableBody-${uuid}"></tbody> </table> </div> <script> (function () { // ========================================================================== // КОНФИГУРАЦИЯ — правьте под свой проект // ========================================================================== var CFG = { // Путь REST API эндпоинта проекта (Источники данных → REST API), // который должен вернуть исторические данные температур за период. restEndpoint: '/restapi/forGraph', // Имена query-параметров, которые виджет отправит на эндпоинт. // Отправляются одновременно ms (unix-время в миллисекундах) и ISO-строка — // используйте то, что удобнее в обработчике. params: { startMs: 'start_ms', endMs: 'end_ms', startIso: 'start_iso', endIso: 'end_iso', tags: 'tags' }, // Температурные кривые. tag = имя устройства в IntraSCADA // (то же самое имя уходит в параметр tags запроса и должно совпадать // с именем, которое ищет holder.getDevice(tag) в обработчике api002). series: [ { tag: 's_047', label: 'sns 1 test', color: '#4fb3d9' }, { tag: 's_048', label: 'sns 2 test', color: '#f2a154' } // Добавляй сюда новые устройства по такому же образцу: // { tag: 's_049', label: 'sns 3', color: '#7fd8a0' }, ], markerCount: 5, markerColors: ['#dbe4ec', '#f2a154', '#7fd8a0', '#c98bde', '#e2694b'] }; // ========================================================================== // Разбор ответа REST API (см. обработчик api002 маршрута /forGraph). // Ожидаемый формат ответа: // { // "res": 1, // "data": { // "s_047": [ { "ts": 1699999999000, "value": 23.4 }, ... ], // "s_048": [ ... ] // } // } // При ошибке на сервере: { "res": 0, "message": "..." } // ========================================================================== function parseApiResponse(json) { console.log("respose " + JSON.stringify(json)) if (!json || json.res === 0) { throw new Error((json && json.message) || 'Сервер вернул ошибку'); } var payload = json.data || {}; var result = {}; CFG.series.forEach(function (s) { var raw = payload[s.tag] || []; var points = raw .map(function (p) { return { ts: Number(p.ts), value: Number(p.value) }; }) .filter(function (p) { return isFinite(p.ts) && isFinite(p.value); }); points.sort(function (a, b) { return a.ts - b.ts; }); result[s.tag] = points; }); return result; } // ========================================================================== // Служебные утилиты // ========================================================================== var uid = '${uuid}'; function $(id) { return document.getElementById(id + '-' + uid); } function pad(n) { return n < 10 ? '0' + n : '' + n; } function fmtRuDateTime(d) { return pad(d.getDate()) + '.' + pad(d.getMonth() + 1) + '.' + d.getFullYear() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); } // Разбор строки формата ДД.ММ.ГГГГ ЧЧ:ММ[:СС] в unix-время (мс). // Возвращает NaN, если строка не соответствует формату или дата некорректна // (например 31.02.2026 не пройдёт проверку после round-trip). function parseRuDateTime(str) { if (!str) return NaN; var m = String(str).trim().match(/^(\d{2})\.(\d{2})\.(\d{4})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/); if (!m) return NaN; var day = Number(m[1]), month = Number(m[2]), year = Number(m[3]); var hour = Number(m[4]), min = Number(m[5]), sec = m[6] ? Number(m[6]) : 0; var d = new Date(year, month - 1, day, hour, min, sec, 0); if (d.getDate() !== day || d.getMonth() !== month - 1 || d.getFullYear() !== year || d.getHours() !== hour || d.getMinutes() !== min) { return NaN; } return d.getTime(); } function fmtDisplay(ts) { var d = new Date(ts); return pad(d.getDate()) + '.' + pad(d.getMonth() + 1) + '.' + d.getFullYear() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); } function fmtTick(ts) { var d = new Date(ts); return pad(d.getHours()) + ':' + pad(d.getMinutes()) + '\n' + pad(d.getDate()) + '.' + pad(d.getMonth() + 1); } // Линейная интерполяция значения серии в момент ts. function valueAt(points, ts) { if (!points || points.length === 0) return null; if (ts <= points[0].ts) return points[0].value; if (ts >= points[points.length - 1].ts) return points[points.length - 1].value; for (var i = 0; i < points.length - 1; i++) { var a = points[i], b = points[i + 1]; if (ts >= a.ts && ts <= b.ts) { if (b.ts === a.ts) return a.value; var frac = (ts - a.ts) / (b.ts - a.ts); return a.value + (b.value - a.value) * frac; } } return null; } // ========================================================================== // Состояние виджета // ========================================================================== var chart = null; var seriesData = {}; // { tag: [{ts,value}, ...] } var rangeStart = null; var rangeEnd = null; var markers = []; // [{ ts, lineEl, handleEl }] var dragging = null; var resizeObserver = null; var elStart = $('dtStart'); var elEnd = $('dtEnd'); var btnLoad = $('btnLoad'); var btnPrint = $('btnPrint'); var elStatus = $('status'); var chartWrap = $('chartWrap'); var chartHolder = $('chartHolder'); var emptyState = $('emptyState'); var canvas = $('canvas'); var tableHead = $('tableHead'); var tableBody = $('tableBody'); function setStatus(text, isError) { elStatus.textContent = text || ''; elStatus.className = 'status' + (isError ? ' error' : ''); } // По умолчанию: начало — сегодня 00:00, конец — сейчас. (function initDefaults() { var now = new Date(); var startDefault = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); elStart.value = fmtRuDateTime(startDefault); elEnd.value = fmtRuDateTime(now); })(); // ========================================================================== // Загрузка Chart.js (один раз на страницу) // ========================================================================== function ensureChartJs(callback) { if (window.Chart) { callback(); return; } var existing = document.getElementById('chartjs-lib'); if (existing) { if (window.Chart) { callback(); return; } existing.addEventListener('load', callback); return; } var script = document.createElement('script'); script.id = 'chartjs-lib'; // Сначала пробуем публичный CDN (актуально при подключении через p2p, // где window.location.hostname === 'p2p.ih-systems.com'). script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; script.addEventListener('load', callback); script.addEventListener('error', function () { // Нет доступа в интернет — берём локальную копию из папки anyfiles // проекта, которая на сервере отдаётся по пути /files/... setStatus('CDN недоступен, загружаю локальную копию Chart.js...'); var localScript = document.createElement('script'); localScript.id = 'chartjs-lib'; // тот же id, чтобы не пытаться грузить дважды localScript.src = '/files/chart.js'; localScript.addEventListener('load', callback); localScript.addEventListener('error', function () { setStatus('Не удалось загрузить библиотеку графиков ни с CDN, ни локально (/files/chart.js)', true); }); document.head.appendChild(localScript); }); document.head.appendChild(script); } // ========================================================================== // Запрос данных через REST API // ========================================================================== function requestData(startMs, endMs) { var tags = CFG.series.map(function (s) { return s.tag; }).join(','); var qs = [ CFG.params.startMs + '=' + encodeURIComponent(startMs), CFG.params.endMs + '=' + encodeURIComponent(endMs), CFG.params.startIso + '=' + encodeURIComponent(new Date(startMs).toISOString()), CFG.params.endIso + '=' + encodeURIComponent(new Date(endMs).toISOString()), CFG.params.tags + '=' + encodeURIComponent(tags) ].join('&'); var url = CFG.restEndpoint + '?' + qs; //window.ihapi.fetch('/restapi/myapi').then(res => res.json()).then(json => console.log(json)) return window.ihapi.fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' } }) //return window.ihapi.fetch(url) .then(function (resp) { // Обработчик кладёт понятный текст ошибки в тело (res:0, message) // даже при HTTP-статусе 500, поэтому сперва пытаемся распарсить JSON. return resp.json().catch(function () { throw new Error('HTTP ' + resp.status); }); }) .then(parseApiResponse); } // ========================================================================== // Построение / обновление графика // ========================================================================== function buildChart() { var datasets = CFG.series.map(function (s) { var points = seriesData[s.tag] || []; return { label: s.label, data: points.map(function (p) { return { x: p.ts, y: p.value }; }), borderColor: s.color, backgroundColor: s.color, borderWidth: 2, pointRadius: 0, tension: 0.15, spanGaps: true }; }); var config = { type: 'line', data: { datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, animation: false, interaction: { mode: 'nearest', axis: 'x', intersect: false }, scales: { x: { type: 'linear', min: rangeStart, max: rangeEnd, ticks: { color: '#8ea0b0', maxTicksLimit: 8, callback: function (val) { return fmtTick(val); } }, grid: { color: 'rgba(255,255,255,0.06)' } }, y: { ticks: { color: '#8ea0b0' }, grid: { color: 'rgba(255,255,255,0.06)' }, title: { display: true, text: '°C', color: '#8ea0b0' } } }, plugins: { legend: { labels: { color: '#dbe4ec', usePointStyle: true, boxWidth: 8, boxHeight: 8 } }, tooltip: { callbacks: { title: function (items) { return fmtDisplay(items[0].parsed.x); }, label: function (ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1) + ' °C'; } } } } } }; if (chart) { chart.data = config.data; chart.options.scales.x.min = rangeStart; chart.options.scales.x.max = rangeEnd; chart.update(); } else { chart = new Chart(canvas.getContext('2d'), config); } } // ========================================================================== // Маркеры: создание, позиционирование, перетаскивание // ========================================================================== function clearMarkers() { markers.forEach(function (m) { if (m.lineEl && m.lineEl.parentNode) m.lineEl.parentNode.removeChild(m.lineEl); }); markers = []; } function createMarkers() { clearMarkers(); var span = rangeEnd - rangeStart; var fractions = [0.1, 0.3, 0.5, 0.7, 0.9].slice(0, CFG.markerCount); fractions.forEach(function (frac, idx) { var ts = rangeStart + span * frac; var color = CFG.markerColors[idx % CFG.markerColors.length]; var line = document.createElement('div'); line.className = 'marker-line'; line.style.borderColor = color; var handle = document.createElement('div'); handle.className = 'marker-handle'; handle.style.background = color; handle.textContent = 'M' + (idx + 1); line.appendChild(handle); chartHolder.appendChild(line); var markerObj = { ts: ts, lineEl: line, handleEl: handle, index: idx, color: color }; attachDrag(markerObj); markers.push(markerObj); }); positionMarkers(); buildTable(); } function positionMarkers() { if (!chart || !chart.chartArea) return; var area = chart.chartArea; var xScale = chart.scales.x; markers.forEach(function (m) { var px = xScale.getPixelForValue(m.ts); px = Math.max(area.left, Math.min(area.right, px)); m.lineEl.style.left = px + 'px'; m.lineEl.style.top = area.top + 'px'; m.lineEl.style.height = (area.bottom - area.top) + 'px'; }); } function attachDrag(m) { function onPointerDown(e) { e.preventDefault(); dragging = m; m.handleEl.setPointerCapture && e.pointerId != null && m.handleEl.setPointerCapture(e.pointerId); document.addEventListener('pointermove', onPointerMove); document.addEventListener('pointerup', onPointerUp); } function onPointerMove(e) { if (!dragging || dragging !== m || !chart || !chart.chartArea) return; var rect = chartHolder.getBoundingClientRect(); var area = chart.chartArea; var x = e.clientX - rect.left; x = Math.max(area.left, Math.min(area.right, x)); var ts = chart.scales.x.getValueForPixel(x); ts = Math.max(rangeStart, Math.min(rangeEnd, ts)); m.ts = ts; m.lineEl.style.left = x + 'px'; updateTableRow(m); } function onPointerUp() { dragging = null; document.removeEventListener('pointermove', onPointerMove); document.removeEventListener('pointerup', onPointerUp); } m.handleEl.addEventListener('pointerdown', onPointerDown); m.lineEl.addEventListener('pointerdown', onPointerDown); } // ========================================================================== // Таблица значений // ========================================================================== function buildTable() { var headRow = ['<th>Маркер</th><th>Время</th>']; CFG.series.forEach(function (s) { headRow.push('<th><span class="marker-dot" style="background:' + s.color + '"></span>' + s.label + ', °C</th>'); }); tableHead.innerHTML = headRow.join(''); tableBody.innerHTML = ''; markers.forEach(function (m) { var tr = document.createElement('tr'); tr.id = 'row-' + uid + '-' + m.index; tableBody.appendChild(tr); updateTableRow(m); }); } function updateTableRow(m) { var tr = document.getElementById('row-' + uid + '-' + m.index); if (!tr) return; var cells = [ '<td><span class="marker-dot" style="background:' + m.color + '"></span>M' + (m.index + 1) + '</td>', '<td>' + fmtDisplay(m.ts) + '</td>' ]; CFG.series.forEach(function (s) { var v = valueAt(seriesData[s.tag], m.ts); cells.push('<td>' + (v === null ? '—' : v.toFixed(1)) + '</td>'); }); tr.innerHTML = cells.join(''); } // ========================================================================== // Основной обработчик кнопки «Показать» // ========================================================================== function onLoadClick() { var startVal = elStart.value; var endVal = elEnd.value; if (!startVal.trim() || !endVal.trim()) { setStatus('Укажите обе даты — начало и конец', true); return; } var startMs = parseRuDateTime(startVal); var endMs = parseRuDateTime(endVal); if (!isFinite(startMs) || !isFinite(endMs)) { setStatus('Неверный формат даты. Используйте ДД.ММ.ГГГГ ЧЧ:ММ:СС', true); return; } if (startMs >= endMs) { setStatus('Начало должно быть раньше конца', true); return; } btnLoad.disabled = true; setStatus('Загрузка данных...'); ensureChartJs(function () { requestData(startMs, endMs) .then(function (data) { seriesData = data; rangeStart = startMs; rangeEnd = endMs; var hasAnyPoints = CFG.series.some(function (s) { return (seriesData[s.tag] || []).length > 0; }); emptyState.style.display = hasAnyPoints ? 'none' : 'flex'; if (!hasAnyPoints) { setStatus('За указанный период данные не найдены', true); btnLoad.disabled = false; btnPrint.disabled = true; return; } buildChart(); createMarkers(); btnPrint.disabled = false; setStatus('Данные загружены: ' + fmtDisplay(startMs) + ' — ' + fmtDisplay(endMs)); }) .catch(function (err) { setStatus('Ошибка запроса: ' + err.message, true); }) .finally(function () { btnLoad.disabled = false; }); }); } function openPrintWindow() { if (!chart) { setStatus('Сначала загрузите данные', true); return; } var imgData = chart.toBase64Image('image/png', 1); var periodText = fmtDisplay(rangeStart) + ' — ' + fmtDisplay(rangeEnd); var rowsHtml = markers.map(function (m) { var cells = ['<td>M' + (m.index + 1) + '</td>', '<td>' + fmtDisplay(m.ts) + '</td>']; CFG.series.forEach(function (s) { var v = valueAt(seriesData[s.tag], m.ts); cells.push('<td>' + (v === null ? '—' : v.toFixed(1)) + '</td>'); }); return '<tr>' + cells.join('') + '</tr>'; }).join(''); var headHtml = ['<th>Маркер</th><th>Время</th>'] .concat(CFG.series.map(function (s) { return '<th>' + s.label + ', °C</th>'; })) .join(''); var printHtml = '<!DOCTYPE html><html><head><meta charset="utf-8">' + '<title>Температуры эксперимента</title>' + '<style>' + 'body{font-family:Arial,Helvetica,sans-serif;color:#111;padding:24px;margin:0;}' + 'h1{font-size:16px;margin:0 0 4px;}' + '.period{font-size:13px;color:#555;margin-bottom:16px;}' + 'img{width:100%;max-width:1000px;display:block;margin-bottom:20px;border:1px solid #ccc;}' + 'table{border-collapse:collapse;width:100%;max-width:1000px;font-size:13px;}' + 'th,td{border:1px solid #999;padding:6px 10px;text-align:left;}' + 'th{background:#eee;}' + '</style></head><body>' + '<h1>Температуры эксперимента</h1>' + '<div class="period">Период: ' + periodText + '</div>' + '<img src="' + imgData + '" alt="График температур" />' + '<table><thead><tr>' + headHtml + '</tr></thead><tbody>' + rowsHtml + '</tbody></table>' + '</body></html>'; // Печатаем через скрытый iframe в текущей странице (без создания // отдельного окна) — так надёжнее работает во встроенном браузере // IntraSCADA и не мешает работе полей ввода на основном экране. var oldFrame = document.getElementById('printFrame-' + uid); if (oldFrame) oldFrame.parentNode.removeChild(oldFrame); var iframe = document.createElement('iframe'); iframe.id = 'printFrame-' + uid; iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; iframe.style.border = '0'; iframe.style.visibility = 'hidden'; document.body.appendChild(iframe); var cleaned = false; function cleanup() { if (cleaned) return; cleaned = true; setTimeout(function () { if (iframe.parentNode) iframe.parentNode.removeChild(iframe); }, 1000); } iframe.onload = function () { try { iframe.contentWindow.focus(); iframe.contentWindow.print(); } catch (e) { setStatus('Ошибка печати: ' + e.message, true); } cleanup(); }; var doc = iframe.contentWindow.document; doc.open(); doc.write(printHtml); doc.close(); // Подстраховка на случай, если onload не сработает в конкретном браузере. setTimeout(function () { if (!cleaned) { try { iframe.contentWindow.focus(); iframe.contentWindow.print(); } catch (e) { /* уже обработано выше при удачном сценарии */ } cleanup(); } }, 500); } btnLoad.addEventListener('click', onLoadClick); btnPrint.addEventListener('click', openPrintWindow); // Перерисовка маркеров при изменении размеров контейнера графика. if (window.ResizeObserver) { resizeObserver = new ResizeObserver(function () { if (chart) { chart.resize(); positionMarkers(); } }); resizeObserver.observe(chartWrap); } else { window.addEventListener('resize', function () { if (chart) { chart.resize(); positionMarkers(); } }); } // Уборка за собой при удалении виджета с экрана (если ihapi доступен). if (window.ihapi && window.ihapi.addEventListener) { window.ihapi.addEventListener(uid, 'destroy', function () { if (resizeObserver) resizeObserver.disconnect(); if (chart) chart.destroy(); }); } })(); </script> Файл для импорта, прикреплю чуть позже
  • Техническая поддержка по системе IntraOPC

    1 Темы
    1 Сообщения
    V
    На этом форуме обсуждаются вопросы использования программного обеспечения (ПО) IntraOPC Сотрудники технической поддержки отвечают в рабочее время (по MSK). Не надо писать свой вопрос в первую попавшуюся тему - всегда лучше создать новую тему. Если вы столкнулись с ошибкой в работе ПО, опишите проблему подробно: На какой операционной системе работает программное обеспечение? Какой компьютер/контроллер используется в качестве сервера? Какая версия програмного обеспечения установлена? Какое поведение ожидаете увидеть? Что происходит в действительности?
  • Техническая поддержка по системе IntraHouse

    6 Темы
    18 Сообщения
    M
    @Sboro Добрый день. Попробуйте из папки /var/lib/intrahouse/папка проекта/private удалить файл users.db и перезагрузить сервис. Это сбросит пароль по умолчанию. И у вас очень старая версия системы. Рекомендую обновить до последней.
  • 1 Темы
    1 Сообщения
    V
    Общие положения 1.1 Форум предназначен для общения по теме использования систем IntraSCADA, IntraOPC и IntraHouse. Участие на форуме является бесплатным и свободным. Регистрация на форуме автоматически означает Ваше согласие с настоящими правилами и с необходимостью их выполнения. 1.2 Незнание правил не освобождает Вас от ответственности. 1.3 Администраторы и модераторы форума стараются удалять или редактировать неприемлемые сообщения как можно быстрее. Но все сообщения просмотреть невозможно. Таким образом Вы признаёте, что сообщения на форуме отражают точки зрения их авторов, а не администрации форума (кроме сообщений, размещённых её представителями) и администрация не может быть ответственна и не несет ответственность за их содержание. Регистрация 2.1 Регистрация на форуме является добровольной. 2.2 В качестве Имени (Ника) участник может выбрать любое слово или словосочетание, не противоречащее здравому смыслу и данным Правилам. 2.3 Запрещено использовать чужое Имя (Ник) и профиль для написания постов. Также запрещается выбирать в качестве собственного Имени (Ника) слова, сочетания и выражения, похожие на Имя (Ник) уже зарегистрированного пользователя. Права участников форума 3.1 Участники форума имеют право размещать сообщения и открывать новые темы, не противоречащие данным правилам и соответствующие тематике форума. 3.2 Участники могут вносить предложения по работе форума. Размещение новых тем. 4.1 Название темы должно быть информативным. В заголовке необходимо обозначить название предмета, которого касается тема, а в описании следует дать подробное её описание или наиболее важную информацию. 4.2 Запрещено создавать темы не соответствующие тематике раздела. Не соответствующие тематике темы будут удалены или перемещены без предупреждения. На форуме запрещено: 5.1 Рекламировать конкретные продукты, услуги или сайты. Рекламой считается сообщение, направленное на создание спроса на какой-либо вид товаров или услуг (в том числе на веб-сайты), а также (в случае веб-сайтов) размещение ссылок на конкретные товары или услуги. 5.2 Призывать к межнациональной, межрелигиозной, межкультурной розни. 5.3 Призывать к нарушению действующего законодательства, а также размещать информацию, нарушающую действующее законодательство РФ. 5.4 Использование ненормативной лексики в ЛЮБЫХ проявлениях. Запрещены любые интерпретации ненормативной лексики, в том числе с использованием символов, подменяющих буквы. 5.5 Оскорблять других участников форума. 5.6 Размещать информацию, способствующую распространению компьютерных вирусов. Заключительные положения 6.1 Все предложения и замечания по работе форума следует отправлять администрации форума, либо модераторам конкретных тем. 6.2 Данные Правила являются обязательными для исполнения ВСЕМИ посетителями форума. 6.3 Администрация вправе вносить изменения в настоящие Правила. Новая редакция вступает в силу с момента опубликования, если не указано иное. Продолжение использования форума после изменений означает согласие с новой редакцией.