Intra Forum
    • Категории
    • Последние
    • Метки
    • Популярные
    • Зарегистрироваться
    • Войти
    1. Главная
    2. grinsva
    3. Темы
    G
    Не в сети
    • Профиль
    • Подписки 0
    • Подписчики 0
    • Темы 5
    • Сообщения 13
    • Группы 0

    Темы

    • G

      Пример ГРАФИКА на HTML виджете.

      Отслеживается Игнорируется Запланировано Прикреплена Закрыта Перенесена HTML виджеты
      7
      3
      0 Голоса
      7 Сообщения
      136 Просмотры
      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> Файл для импорта, прикреплю чуть позже
    • G

      Привязка локальных переменных для Графиков в HTML

      Отслеживается Игнорируется Запланировано Прикреплена Закрыта Перенесена HTML виджеты
      4
      2
      0 Голоса
      4 Сообщения
      67 Просмотры
      M
      Да вы все делаете правильно. Нужно еще добавить window.ihapi.addEventListener(uuid, "data", onData); чтобы при изменении переменных клиента вызывалась функция onData и данные обновлялись
    • G

      Корявое изображение в Графиках CHART Multiline GL при зумировании по оси Х

      Отслеживается Игнорируется Запланировано Прикреплена Закрыта Перенесена IntraSCADA
      6
      2
      0 Голоса
      6 Сообщения
      458 Просмотры
      G
      @maksimvershinin у меня так и так через него время получает. Но это не помогало. Пусть решение и затычка каких то проблем, но раз иного нет, то может кому то и поможет. NULL, как я понял, обрывает соединительную линию между точками. Этим я и воспользовался. для того что красиво смотрелось.
    • G

      Появиться ли возможность обновить виджет REPORT из скрипта визуализации?

      Отслеживается Игнорируется Запланировано Прикреплена Закрыта Перенесена IntraSCADA
      2
      1
      0 Голоса
      2 Сообщения
      304 Просмотры
      M
      Добрый день. В новой версии системы 5.19.4 добавили в HTML виджет возможность вызывать Команду Элемента window.ihapi.elementCommand() . Для решения вашей задачи, можно добавить HTML виджет на экран или контейнер, спрятать его на задний фон, подписаться в нем на изменение переменной клиента, которая привязана к дереву и вызывать Команду Элемента Submit для Отчета. Пример Виджета: <style type="text/css"> </style> <script type="text/javascript"> function destroy() { window.ihapi.deviceUnsub(uuid, ['local003_var846']) } function update(data) { console.log(data) window.ihapi.elementCommand('report_1', 'submit') } function init() { window.ihapi.addEventListener(uuid, 'destroy', destroy) window.ihapi.addEventListener(uuid, 'data', update) window.ihapi.deviceSub(uuid, ['local003_var846']) } init() </script> <div id="${uuid}">Hello World!</div> Так же есть возможность использовать данную команду при нажатии на кнопку. Для этого нужно добавить элемент Кнопка на экран, на нажатие по клику выбрать Установить значение, выбрать любую переменную клиента и поле function прописать данную команду window.ihapi.elementCommand('report_1', 'submit')
    • G

      Изменение атрибутов сохранения в БД для устройства из скрипта/сценария

      Отслеживается Игнорируется Запланировано Прикреплена Закрыта Перенесена IntraSCADA
      5
      0 Голоса
      5 Сообщения
      487 Просмотры
      I
      @grinsva, добрый день! Вышла версия 5.19.6, в нее добавили метод устройства device.updateDbrule для изменения правил сохранения свойств. Описание и примеры в документации