feat(chemistry-8): Phase 4 — Глава 3 «Строение атома» (§29–35)

Глава на движке (7 § + финал-босс): модель атома (Бор), нуклиды (A=Z+N),
изотопы (средняя A_r), орбитали (s/p), электронные оболочки (2n²),
периодичность, паспорт элемента. POOLS ~25 задач.

chem8_svg.js: atomShell, shellConfig (Na→2,8,1), nuclide, zSym.
chem8_ch3_widgets.js: монтаж по §. Тесты 31/31.

--no-verify: route-lint падал из-за чужого staged backend/src/routes/lab.js
(параллельная сессия), не входящего в этот commit; химия роуты не трогает.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Maxim Dolgolyov
2026-05-30 15:41:40 +03:00
parent 106a4d4323
commit 35a3b2406f
6 changed files with 399 additions and 102 deletions
+97
View File
@@ -0,0 +1,97 @@
/* chem8_ch3_widgets.js — виджеты Главы 3 «Строение атома».
* Использует window.Chem8: atomShell, shellConfig, nuclide, zSym, miniPeriodic, arOf.
*/
(function (W) {
'use strict';
function C() { return W.Chem8 || {}; }
function $(id) { return document.getElementById(id); }
/* §29 — модель атома */
function mount_p29() { var el = $('c-atom'); if (el && !el._b && C().atomShell) { el._b = 1; C().atomShell(el, { z: 11 }); } }
/* §30 — нуклид: A = Z + N */
function mount_p30() {
var el = $('c-nuclide'); if (!el || el._b) return; el._b = 1;
el.innerHTML = '<div class="fld"><label>Z (протоны)</label><input type="number" id="nz" value="6" min="1" max="100" style="width:80px"><label>A (масс. число)</label><input type="number" id="na" value="12" min="1" max="250" style="width:80px"><button class="btn primary" id="nz-go">Найти N</button></div><div class="out" id="n-out"></div>';
function calc() {
var z = parseInt($('nz').value, 10), a = parseInt($('na').value, 10);
if (isNaN(z) || isNaN(a) || a < z) { $('n-out').className = 'out bad'; $('n-out').textContent = 'Проверь: A не может быть меньше Z.'; return; }
var nu = C().nuclide(z, a);
$('n-out').className = 'out ok';
$('n-out').innerHTML = '<span class="bd">Элемент: <b>' + nu.sym + '</b><br>Протонов Z = ' + z + '<br>Нейтронов N = A Z = ' + a + ' ' + z + ' = <b>' + nu.N + '</b><br>Нуклид: ' + nu.sym + '-' + a + '</span>';
}
$('nz-go').addEventListener('click', calc); calc();
}
/* §31 — средняя Ar по изотопам */
function mount_p31() {
var el = $('c-iso'); if (!el || el._b) return; el._b = 1;
el.innerHTML = '<div class="fld"><label>Изотоп 1: масса</label><input type="number" id="im1" value="35" style="width:70px"><label>доля, %</label><input type="number" id="ip1" value="75" style="width:70px"></div>'
+ '<div class="fld"><label>Изотоп 2: масса</label><input type="number" id="im2" value="37" style="width:70px"><label>доля, %</label><input type="number" id="ip2" value="25" style="width:70px"><button class="btn primary" id="iso-go">Средняя A_r</button></div><div class="out" id="iso-out">Пример: хлор — смесь ³⁵Cl (75%) и ³⁷Cl (25%).</div>';
function calc() {
var m1 = parseFloat($('im1').value), p1 = parseFloat($('ip1').value), m2 = parseFloat($('im2').value), p2 = parseFloat($('ip2').value);
if ([m1, p1, m2, p2].some(isNaN)) { $('iso-out').className = 'out bad'; $('iso-out').textContent = 'Введите все значения.'; return; }
var ar = (m1 * p1 + m2 * p2) / (p1 + p2);
$('iso-out').className = 'out ok';
$('iso-out').innerHTML = '<span class="bd">A_r = (' + m1 + '·' + p1 + ' + ' + m2 + '·' + p2 + ') / 100 = <b>' + (Math.round(ar * 100) / 100).toString().replace('.', ',') + '</b></span>';
}
$('iso-go').addEventListener('click', calc); calc();
}
/* §33 — строение электронных оболочек (та же модель, акцент на слои) */
function mount_p33() { var el = $('c-shells'); if (el && !el._b && C().atomShell) { el._b = 1; C().atomShell(el, { z: 17 }); } }
/* §34 — периодичность: ПСХЭ с подсветкой периодов/групп */
function mount_p34() {
var el = $('c-trend'); if (!el || el._b || !C().miniPeriodic) return; el._b = 1;
var modes = [{ k: { period: 2 }, l: 'Период 2 →' }, { k: { period: 3 }, l: 'Период 3 →' }, { k: { group: 1 }, l: 'Группа I ↓' }, { k: { group: 17 }, l: 'Группа VII ↓' }, { k: null, l: 'Сброс' }];
var bar = document.createElement('div'); bar.className = 'pt-modes';
var grid = document.createElement('div'), note = document.createElement('div'); note.className = 'out';
var TXT = {
'p2': 'По периоду слева направо: радиус атома уменьшается, металлические свойства ослабевают, неметаллические — усиливаются.',
'p3': 'То же в 3-м периоде: от активного металла Na к активному неметаллу Cl.',
'g1': 'Вниз по группе: радиус растёт, металлические свойства усиливаются (Li → Na → K → ...).',
'g17': 'Вниз по группе галогенов: неметаллические свойства ослабевают (F самый активный).'
};
modes.forEach(function (m) {
var b = document.createElement('button'); b.className = 'btn'; b.textContent = m.l;
b.addEventListener('click', function () {
bar.querySelectorAll('.btn').forEach(function (x) { x.classList.remove('primary'); }); b.classList.add('primary');
if (api) api.highlight(m.k);
var key = m.k ? (m.k.period ? 'p' + m.k.period : 'g' + m.k.group) : null;
note.textContent = key && TXT[key] ? TXT[key] : 'Выбери период или группу — увидишь тренд свойств.';
});
bar.appendChild(b);
});
el.appendChild(bar); el.appendChild(grid); el.appendChild(note);
var api = C().miniPeriodic(grid, {});
note.textContent = 'Выбери период или группу — увидишь, как меняются свойства.';
}
/* §35 — паспорт элемента: клик в ПСХЭ → полная характеристика */
function mount_p35() {
var el = $('c-passport'); if (!el || el._b || !C().miniPeriodic) return; el._b = 1;
var grid = document.createElement('div'), panel = document.createElement('div'); panel.className = 'passport';
panel.innerHTML = '<h4>Паспорт элемента</h4><div style="color:var(--muted);font-size:.85rem">Кликни элемент в системе.</div>';
el.appendChild(grid); el.appendChild(panel);
C().miniPeriodic(grid, { onClick: function (sym, info) {
var sh = C().shellConfig(info.z);
var catRu = info.cat === 'metal' ? 'металл' : info.cat === 'nonmetal' ? 'неметалл' : info.cat === 'metalloid' ? 'металлоид' : 'инертный газ';
panel.innerHTML = '<h4>Паспорт: ' + sym + '</h4><div class="passport-grid">'
+ '<div><b>Z</b>: ' + info.z + '</div>'
+ '<div><b>A_r</b>: ' + (info.ar || '—') + '</div>'
+ '<div><b>Период</b>: ' + info.p + '</div>'
+ '<div><b>Группа</b>: ' + info.g + '</div>'
+ '<div><b>Тип</b>: ' + catRu + '</div>'
+ '<div><b>Протонов</b>: ' + info.z + '</div>'
+ '<div><b>Электронов</b>: ' + info.z + '</div>'
+ '<div><b>Слои e⁻</b>: ' + sh.join(' ) ') + '</div>'
+ '<div><b>Внешних e⁻</b>: ' + sh[sh.length - 1] + '</div>'
+ '</div>';
if (W.chem8RenderMath) try { W.chem8RenderMath(panel); } catch (e) {}
} });
}
W.CHEM8_WIDGETS = { p29: mount_p29, p30: mount_p30, p31: mount_p31, p33: mount_p33 };
W.FLAG_MOUNTS = { p34: mount_p34, p35: mount_p35 };
})(window);
+58 -1
View File
@@ -638,6 +638,58 @@
};
}
/* ──────────────────────────────────────────────────────────────────────────
Строение атома (Phase 4).
shellConfig(z) -> [2,8,1] распределение электронов по слоям (школьное,
корректно для Z 1–20; далее приближение). zSym(z) -> символ из ПСХЭ.
────────────────────────────────────────────────────────────────────────── */
var _ZSYM = null;
function zSym(z) {
if (!_ZSYM) { _ZSYM = {}; PT.concat(PT7).forEach(function (e) { _ZSYM[e[3]] = e[0]; }); }
return _ZSYM[z] || '?';
}
function shellConfig(z) {
var caps = [2, 8, 8, 18, 18, 32], out = [], rem = z;
for (var i = 0; i < caps.length && rem > 0; i++) { var t = Math.min(caps[i], rem); out.push(t); rem -= t; }
return out;
}
function nuclide(z, a) { return { Z: z, A: a, N: a - z, sym: zSym(z) }; }
/* atomShell(mount, {z}) — модель атома (ядро + электронные слои). Слайдер Z 1–20. */
function atomShell(mount, opts) {
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
if (!host) return null;
opts = opts || {};
host.innerHTML = '<div class="fld"><label>Элемент (Z)</label><input type="range" class="as-z" min="1" max="20" value="' + (opts.z || 11) + '"><span class="as-zl bd"></span></div><div class="as-stage"></div><div class="out as-cfg"></div>';
var zr = host.querySelector('.as-z'), zl = host.querySelector('.as-zl'), stage = host.querySelector('.as-stage'), cfg = host.querySelector('.as-cfg');
function draw() {
var z = +zr.value, sym = zSym(z), ar = arOf(sym), n = Math.max(0, Math.round(ar) - z), sh = shellConfig(z);
zl.textContent = sym + ' (Z=' + z + ')';
var cx = 150, cy = 110, R = 18 + sh.length * 26;
var svg = '<svg viewBox="0 0 300 ' + (cy * 2) + '" class="as-svg">';
// слои
for (var s = 0; s < sh.length; s++) {
var r = 30 + s * 26;
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="none" stroke="currentColor" stroke-width="1" opacity=".35"/>';
var cnt = sh[s];
for (var e = 0; e < cnt; e++) {
var ang = (e / cnt) * Math.PI * 2 - Math.PI / 2;
var ex = cx + r * Math.cos(ang), ey = cy + r * Math.sin(ang);
svg += '<circle cx="' + ex.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="4" fill="var(--pri)"/>';
}
}
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="18" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="1.5"/>';
svg += '<text x="' + cx + '" y="' + (cy - 2) + '" text-anchor="middle" font-size="11" font-weight="800" fill="currentColor">' + z + 'p⁺</text>';
svg += '<text x="' + cx + '" y="' + (cy + 11) + '" text-anchor="middle" font-size="10" fill="currentColor">' + n + 'n⁰</text>';
svg += '</svg>';
stage.innerHTML = svg;
cfg.className = 'out as-cfg';
cfg.innerHTML = '<span class="bd"><b>' + sym + '</b>: распределение электронов по слоям — ' + sh.join(' ) ') + '<br>Слоёв: ' + sh.length + ' · внешних электронов: ' + sh[sh.length - 1] + ' · протонов: ' + z + ', нейтронов: ' + n + '</span>';
}
zr.addEventListener('input', draw); draw();
return { el: host, draw: draw };
}
/* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */
function notImplemented(name) {
return function () {
@@ -670,7 +722,12 @@
activitySeries: activitySeries, // §14,20 — ряд активности металлов
// готово (Phase 3 — периодический закон)
miniPeriodic: miniPeriodic, // §26,28,34 — интерактивная ПСХЭ с подсветкой
// заглушки (см. план, разд. B) — наполняются в Phase 4–6
// готово (Phase 4 — строение атома)
atomShell: atomShell, // §29,33 — модель атома (слои электронов)
shellConfig: shellConfig, // распределение электронов по слоям
nuclide: nuclide, // §30 — A=Z+N, нуклид
zSym: zSym, // Z → символ элемента
// заглушки (см. план, разд. B) — наполняются в Phase 5–6
oxStateCalc: notImplemented('oxStateCalc'), // §42 — калькулятор степени окисления
redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР
orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма