Фикс вращения вентилятора + разделение сигналов на аналоговые/дискретные
transform-origin: center у SVG-группы лопастей считался от bounding box, а не от геометрического центра ступицы (0,0) — вентилятор вращался вокруг смещённой точки. Зафиксировано в 0 0. Датчик больше не подписан как «DHT22» (сбивало с толку при добавлении дискретных сигналов вроде door_open) — просто «Датчик», а сами сигналы в карточке визуально разбиты на группы «Аналоговые»/«Дискретные».
This commit is contained in:
@@ -47,7 +47,12 @@
|
|||||||
.peripheral-label { font-size: 12px; fill: var(--text); font-weight: 600; text-anchor: middle; }
|
.peripheral-label { font-size: 12px; fill: var(--text); font-weight: 600; text-anchor: middle; }
|
||||||
.peripheral-sub { font-size: 9px; fill: var(--muted); text-anchor: middle; }
|
.peripheral-sub { font-size: 9px; fill: var(--muted); text-anchor: middle; }
|
||||||
|
|
||||||
.fan-blades { transform-origin: center; transition: opacity .2s; }
|
/* The blade ellipses are laid out so their geometric centroid is the
|
||||||
|
group's local (0,0) — but CSS "transform-origin: center" for SVG uses
|
||||||
|
the bounding box center, which isn't the same point (the ellipses'
|
||||||
|
144x28-ish extents aren't symmetric around 0,0), so it wobbled around
|
||||||
|
an off-center point instead of spinning cleanly. Pin it to 0 0. */
|
||||||
|
.fan-blades { transform-origin: 0px 0px; transition: opacity .2s; }
|
||||||
.fan-blades.on { animation: spin 0.9s linear infinite; }
|
.fan-blades.on { animation: spin 0.9s linear infinite; }
|
||||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
@@ -107,6 +112,12 @@
|
|||||||
.value { color: var(--text); font-weight: 600; }
|
.value { color: var(--text); font-weight: 600; }
|
||||||
.hint { color: var(--muted); font-size: 0.78rem; margin-top: 10px; }
|
.hint { color: var(--muted); font-size: 0.78rem; margin-top: 10px; }
|
||||||
|
|
||||||
|
.signal-group-label {
|
||||||
|
font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
color: var(--muted); margin-top: 12px; margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.signal-group-label:first-child { margin-top: 0; }
|
||||||
|
|
||||||
.signal-row {
|
.signal-row {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
gap: 8px; padding: 6px 0; border-top: 1px solid var(--border); font-size: 0.85rem;
|
gap: 8px; padding: 6px 0; border-top: 1px solid var(--border); font-size: 0.85rem;
|
||||||
@@ -184,7 +195,7 @@
|
|||||||
<text id="board-sensor-primary" x="0" y="-2" text-anchor="middle" font-size="14" fill="#e6edf5" font-weight="700">24°C</text>
|
<text id="board-sensor-primary" x="0" y="-2" text-anchor="middle" font-size="14" fill="#e6edf5" font-weight="700">24°C</text>
|
||||||
<text id="board-sensor-secondary" x="0" y="14" text-anchor="middle" font-size="10" fill="#8ea0b8">55%</text>
|
<text id="board-sensor-secondary" x="0" y="14" text-anchor="middle" font-size="10" fill="#8ea0b8">55%</text>
|
||||||
</g>
|
</g>
|
||||||
<text class="peripheral-label" x="70" y="118">Датчик DHT22</text>
|
<text class="peripheral-label" x="70" y="118">Датчик</text>
|
||||||
<text class="peripheral-sub" x="70" y="130">GPIO4 · input</text>
|
<text class="peripheral-sub" x="70" y="130">GPIO4 · input</text>
|
||||||
|
|
||||||
<!-- fan (top right) -->
|
<!-- fan (top right) -->
|
||||||
@@ -289,17 +300,12 @@ function cardHTML(slot) {
|
|||||||
|
|
||||||
document.getElementById('cards').innerHTML = Object.keys(SLOTS).map(cardHTML).join('');
|
document.getElementById('cards').innerHTML = Object.keys(SLOTS).map(cardHTML).join('');
|
||||||
|
|
||||||
function signalRowHTML(slot, name, value) {
|
function isDiscreteSignal(slot, name) {
|
||||||
const preset = SIGNAL_PRESETS[name];
|
return knownSignals[slot]?.discrete?.has(name);
|
||||||
const isDiscrete = !preset && (value === 0 || value === 1) && knownSignals[slot]?.discrete?.has(name);
|
|
||||||
|
|
||||||
if (isDiscrete) {
|
|
||||||
return `<div class="signal-row" data-signal="${name}">
|
|
||||||
<span class="name">${name}</span>
|
|
||||||
<div class="toggle ${value ? 'on' : ''}" onclick="toggleDiscrete('${slot}','${name}')"><div class="knob"></div></div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function analogRowHTML(slot, name, value) {
|
||||||
|
const preset = SIGNAL_PRESETS[name];
|
||||||
const min = preset?.min ?? 0, max = preset?.max ?? 100, step = preset?.step ?? 1, unit = preset?.unit ?? '';
|
const min = preset?.min ?? 0, max = preset?.max ?? 100, step = preset?.step ?? 1, unit = preset?.unit ?? '';
|
||||||
return `<div class="signal-row" data-signal="${name}">
|
return `<div class="signal-row" data-signal="${name}">
|
||||||
<span class="name">${name}${unit ? ' ('+unit+')' : ''}</span>
|
<span class="name">${name}${unit ? ' ('+unit+')' : ''}</span>
|
||||||
@@ -309,14 +315,37 @@ function signalRowHTML(slot, name, value) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discreteRowHTML(slot, name, value) {
|
||||||
|
return `<div class="signal-row" data-signal="${name}">
|
||||||
|
<span class="name">${name}</span>
|
||||||
|
<div class="toggle ${value ? 'on' : ''}" onclick="toggleDiscrete('${slot}','${name}')"><div class="knob"></div></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function cssId(name) { return name.replace(/[^a-zA-Z0-9_-]/g, '_'); }
|
function cssId(name) { return name.replace(/[^a-zA-Z0-9_-]/g, '_'); }
|
||||||
|
|
||||||
|
// Discrete vs analog are shown as two visually separate groups — a
|
||||||
|
// discrete "door_open" signal is still just another sensor_type on the
|
||||||
|
// same physical device underneath (one MQTT device can report several
|
||||||
|
// sensor_types, same as real telemetry), but grouping keeps it from
|
||||||
|
// reading as "attached to the temperature sensor".
|
||||||
function renderSignals(slot, readings) {
|
function renderSignals(slot, readings) {
|
||||||
const container = document.getElementById(`${slot}-signals`);
|
const container = document.getElementById(`${slot}-signals`);
|
||||||
const names = Object.keys(readings);
|
|
||||||
// Preserve discrete-ness across re-renders (server only stores numbers).
|
|
||||||
knownSignals[slot] = knownSignals[slot] || { discrete: new Set() };
|
knownSignals[slot] = knownSignals[slot] || { discrete: new Set() };
|
||||||
container.innerHTML = names.map((name) => signalRowHTML(slot, name, readings[name])).join('');
|
|
||||||
|
const analogNames = orderedNames(readings).filter((n) => !isDiscreteSignal(slot, n));
|
||||||
|
const discreteNames = Object.keys(readings).filter((n) => isDiscreteSignal(slot, n)).sort();
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
if (analogNames.length) {
|
||||||
|
html += `<div class="signal-group-label">Аналоговые</div>`;
|
||||||
|
html += analogNames.map((name) => analogRowHTML(slot, name, readings[name])).join('');
|
||||||
|
}
|
||||||
|
if (discreteNames.length) {
|
||||||
|
html += `<div class="signal-group-label">Дискретные</div>`;
|
||||||
|
html += discreteNames.map((name) => discreteRowHTML(slot, name, readings[name])).join('');
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
updateBoardText(slot, readings);
|
updateBoardText(slot, readings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user