diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js
index 3ab7f5c..8a18dc9 100644
--- a/backend/tests/chemistry8-page.test.js
+++ b/backend/tests/chemistry8-page.test.js
@@ -129,3 +129,15 @@ test('ch4: SPA без ошибок, 7 карточек, §36 активен, т
doc.defaultView.goTo('p38'); await wait(120);
assert.ok(doc.querySelector('#c-bond2 .bt-out'), 'виджет полярности §38');
});
+
+/* ── Глава 5 ── */
+test('ch5: SPA без ошибок, 5 карточек, §42 активен, с.о. и баланс', async () => {
+ const { doc, errors } = await loadDom('chemistry_8_ch5.html', '/js/chem8_ch5_widgets.js');
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+ assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 5, '4 § + финал');
+ assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p42', '§42 активен');
+ await wait(120);
+ assert.ok(doc.querySelector('#c-ox .ox-out'), 'калькулятор с.о. §42');
+ doc.defaultView.goTo('p44'); await wait(120);
+ assert.ok(doc.querySelector('#c-redox-pick option'), 'электронный баланс §44');
+});
diff --git a/backend/tests/chemistry8.test.js b/backend/tests/chemistry8.test.js
index 7d58d42..b39f87a 100644
--- a/backend/tests/chemistry8.test.js
+++ b/backend/tests/chemistry8.test.js
@@ -65,7 +65,7 @@ test('Chem8.elementCounts — скобки и индексы', () => {
});
test('Chem8 — оставшиеся заглушки возвращают null и не падают', () => {
- for (const fn of ['oxStateCalc', 'redoxBalancer', 'orbitalDiagram', 'dissociationAnim', 'geneticMap']) {
+ for (const fn of ['redoxBalancer', 'orbitalDiagram', 'dissociationAnim', 'geneticMap']) {
assert.equal(typeof C[fn], 'function', fn + ' определён');
assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null');
}
@@ -113,7 +113,7 @@ test('каждая глава существует, ссылается на ха
const html = fs.readFileSync(path.join(TB, ch.file), 'utf8');
assert.ok(html.includes('/textbook/chemistry-8"'), ch.file + ' ссылка назад в хаб');
assert.ok(html.includes('/js/chem8_svg.js'), ch.file + ' подключает chem8_svg');
- if (['chemistry-8-intro', 'chemistry-8-ch1', 'chemistry-8-ch2', 'chemistry-8-ch3', 'chemistry-8-ch4'].includes(ch.slug)) {
+ if (['chemistry-8-intro', 'chemistry-8-ch1', 'chemistry-8-ch2', 'chemistry-8-ch3', 'chemistry-8-ch4', 'chemistry-8-ch5'].includes(ch.slug)) {
// перестроены на движок (SPA): slug задаётся через CHEM8_CFG
assert.ok(html.includes("slug:'" + ch.slug + "'"), ch.file + ' slug в CHEM8_CFG');
assert.ok(html.includes('/js/chem8_engine.js'), ch.file + ' подключает движок');
@@ -186,6 +186,17 @@ test('Phase 5 — Глава 4 построена + bondType корректен'
assert.equal(C.bondClass('Na', 'Cl').type, 'ионная');
});
+test('Phase 6 — Глава 5 построена + oxStates корректен', () => {
+ const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch5.html'), 'utf8');
+ for (let i = 42; i <= 45; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
+ assert.ok(html.includes('id="c-ox"'), 'калькулятор с.о. §42');
+ assert.ok(html.includes('id="c-redox-pick"'), 'электронный баланс §44');
+ assert.ok(html.includes('/js/chem8_ch5_widgets.js'), 'виджеты главы 5');
+ assert.equal(C.oxStates('H2SO4').S, 6, 'S в H₂SO₄ = +6');
+ assert.equal(C.oxStates('KMnO4').Mn, 7, 'Mn в KMnO₄ = +7');
+ assert.equal(C.oxStates('HNO3').N, 5, 'N в HNO₃ = +5');
+});
+
test('chem8_engine.js и виджеты — валидный синтаксис', () => {
const eng = fs.readFileSync(path.join(ROOT, 'frontend', 'js', 'chem8_engine.js'), 'utf8');
const wid = fs.readFileSync(path.join(ROOT, 'frontend', 'js', 'chem8_intro_widgets.js'), 'utf8');
diff --git a/frontend/js/chem8_ch5_widgets.js b/frontend/js/chem8_ch5_widgets.js
new file mode 100644
index 0000000..5639825
--- /dev/null
+++ b/frontend/js/chem8_ch5_widgets.js
@@ -0,0 +1,58 @@
+/* chem8_ch5_widgets.js — виджеты Главы 5 «Окислительно-восстановительные реакции».
+ * Использует window.Chem8: oxStateCalc.
+ */
+(function (W) {
+ 'use strict';
+ function C() { return W.Chem8 || {}; }
+ function $(id) { return document.getElementById(id); }
+
+ /* §42 — калькулятор степени окисления */
+ function mount_p42() { var el = $('c-ox'); if (el && !el._b && C().oxStateCalc) { el._b = 1; C().oxStateCalc(el, { formula: 'H2SO4' }); } }
+
+ /* §44 — пошаговый электронный баланс (преднабор) */
+ var R = [
+ { eq: '2Mg + O₂ → 2MgO',
+ steps: [
+ 'Степени окисления: Mg⁰, O₂⁰ → Mg⁺², O⁻².',
+ 'Mg⁰ − 2e⁻ → Mg⁺² — окисление (Mg — восстановитель).',
+ 'O₂⁰ + 4e⁻ → 2O⁻² — восстановление (O₂ — окислитель).',
+ 'Электронный баланс: отдано 2e⁻ (×2 = 4), принято 4e⁻ → множители 2 и 1.',
+ 'Коэффициенты: 2Mg + O₂ → 2MgO. ✓'
+ ] },
+ { eq: 'Fe + CuSO₄ → FeSO₄ + Cu',
+ steps: [
+ 'Меняют с.о. только Fe и Cu: Fe⁰ → Fe⁺², Cu⁺² → Cu⁰.',
+ 'Fe⁰ − 2e⁻ → Fe⁺² — окисление (Fe — восстановитель).',
+ 'Cu⁺² + 2e⁻ → Cu⁰ — восстановление (Cu⁺² — окислитель).',
+ 'Отдано 2e⁻ = принято 2e⁻ → множители 1 и 1.',
+ 'Коэффициенты: Fe + CuSO₄ → FeSO₄ + Cu. ✓'
+ ] },
+ { eq: '2Na + Cl₂ → 2NaCl',
+ steps: [
+ 'Na⁰ и Cl₂⁰ → Na⁺ и Cl⁻.',
+ 'Na⁰ − 1e⁻ → Na⁺ — окисление (Na — восстановитель).',
+ 'Cl₂⁰ + 2e⁻ → 2Cl⁻ — восстановление (Cl₂ — окислитель).',
+ 'Баланс: 1e⁻ ×2 = 2e⁻ → множители 2 и 1.',
+ 'Коэффициенты: 2Na + Cl₂ → 2NaCl. ✓'
+ ] }
+ ];
+ function mount_p44() {
+ var pick = $('c-redox-pick'), out = $('c-redox-out'), bStep = $('c-redox-step'), bAll = $('c-redox-all'); if (!pick || pick._b) return; pick._b = 1;
+ R.forEach(function (p, i) { var o = document.createElement('option'); o.value = i; o.textContent = p.eq; pick.appendChild(o); });
+ var cur = 0, shown = 0;
+ function render() {
+ var p = R[cur];
+ var html = '' + p.eq + '
';
+ for (var i = 0; i < shown; i++) html += '
' + p.steps[i] + '
';
+ if (shown === 0) html += '
Нажимай «Следующий шаг» — разберём метод электронного баланса. ';
+ html += '
'; out.className = shown >= p.steps.length ? 'out ok' : 'out'; out.innerHTML = html;
+ }
+ pick.addEventListener('change', function () { cur = +pick.value; shown = 0; render(); });
+ bStep.addEventListener('click', function () { if (shown < R[cur].steps.length) { shown++; render(); } });
+ bAll.addEventListener('click', function () { shown = R[cur].steps.length; render(); });
+ render();
+ }
+
+ W.CHEM8_WIDGETS = { p42: mount_p42 };
+ W.FLAG_MOUNTS = { p44: mount_p44 };
+})(window);
diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js
index daba5d3..ed833cb 100644
--- a/frontend/js/chem8_svg.js
+++ b/frontend/js/chem8_svg.js
@@ -747,6 +747,59 @@
return { el: host, update: upd };
}
+ /* ──────────────────────────────────────────────────────────────────────────
+ Степень окисления (Phase 6).
+ oxStates(formula) -> {el: oxidation} для типичных нейтральных соединений.
+ Правила: F=−1, O=−2, H=+1, щелочные=+1, ЩЗМ=+2, Al=+3; галогены=−1 без O;
+ остаток решается из условия Σ(с.о.·индекс)=0. oxStateCalc — виджет.
+ ────────────────────────────────────────────────────────────────────────── */
+ var OX_FIX = { F:-1, O:-2, H:1, Li:1, Na:1, K:1, Rb:1, Cs:1, Be:2, Mg:2, Ca:2, Sr:2, Ba:2, Al:3, Zn:2, Ag:1 };
+ function oxStates(formula) {
+ var c = elementCounts(String(formula || '').replace(/\s+/g, ''));
+ var keys = Object.keys(c); if (!keys.length) return null;
+ var hasO = !!c.O, res = {}, unknown = [], sumFixed = 0;
+ keys.forEach(function (el) {
+ var v;
+ if (Object.prototype.hasOwnProperty.call(OX_FIX, el)) v = OX_FIX[el];
+ else if ((el === 'Cl' || el === 'Br' || el === 'I') && !hasO) v = -1;
+ else { unknown.push(el); return; }
+ res[el] = v; sumFixed += v * c[el];
+ });
+ if (unknown.length === 1) {
+ var el = unknown[0];
+ res[el] = -sumFixed / c[el];
+ } else if (unknown.length > 1) {
+ return { partial: true, known: res, unknown: unknown };
+ }
+ return res;
+ }
+ function oxSign(v) { return (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v); }
+ function oxStateCalc(mount, opts) {
+ var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
+ if (!host) return null;
+ opts = opts || {};
+ host.innerHTML = 'Формула Определить
'
+ + 'H₂O CO₂ Fe₂O₃ KMnO₄ HNO₃
'
+ + '
';
+ var inp = host.querySelector('.ox-in'), out = host.querySelector('.ox-out'), go = host.querySelector('.ox-go');
+ function calc() {
+ var f = inp.value.trim(), r = oxStates(f);
+ if (!r) { out.className = 'out ox-out bad'; out.textContent = 'Не удалось разобрать формулу.'; return; }
+ if (r.partial) {
+ out.className = 'out ox-out bad';
+ out.innerHTML = 'Несколько неизвестных элементов (' + r.unknown.join(', ') + ') — для 8 класса возьми более простое соединение.';
+ return;
+ }
+ out.className = 'out ox-out ok';
+ out.innerHTML = '' + Object.keys(r).map(function (el) { return el + ': ' + oxSign(r[el]) + ' '; }).join(' ') + ' ';
+ }
+ go.addEventListener('click', calc);
+ inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
+ host.querySelectorAll('.ox-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
+ calc();
+ return { el: host };
+ }
+
/* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */
function notImplemented(name) {
return function () {
@@ -788,9 +841,11 @@
bondType: bondType, // §37,38 — ЭО → тип связи
bondClass: bondClass, // классификация связи по ΔЭО
enOf: enOf, // электроотрицательность
- // заглушки (см. план, разд. B) — наполняются в Phase 5–6
- oxStateCalc: notImplemented('oxStateCalc'), // §42 — калькулятор степени окисления
- redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР
+ // готово (Phase 6 — ОВР)
+ oxStateCalc: oxStateCalc, // §42 — калькулятор степени окисления
+ oxStates: oxStates, // степени окисления (чистая функция)
+ // заглушки (см. план, разд. B) — наполняются позже
+ redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР (пошагово в ch5)
orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма
dissociationAnim: notImplemented('dissociationAnim'),// §47 — анимация растворения
geneticMap: notImplemented('geneticMap') // §22 — генетическая карта-граф классов
diff --git a/frontend/textbooks/chemistry_8_ch5.html b/frontend/textbooks/chemistry_8_ch5.html
index 2adbfcd..2a5b82d 100644
--- a/frontend/textbooks/chemistry_8_ch5.html
+++ b/frontend/textbooks/chemistry_8_ch5.html
@@ -7,127 +7,177 @@
Химия 8 · Глава 5 · «Окислительно-восстановительные реакции»
-
+
+
+
-
+
+
-
-
-
- К разделам
-
+
-
Глава 5 · § 42–45
-
Окислительно-восстановительные реакции
+
Химия 8 · Глава 5
+
Степень окисления, процессы окисления и восстановления, ОВР и метод электронного баланса
-
-
-
-
-
Раздел в разработке
-
Интерактивное наглядное наполнение этого раздела (теория, модели, симуляторы, тренажёры и боссы) добавляется поэтапно. Ниже — план параграфов раздела согласно учебнику.
-
-
+
+
+
+ Реакции, в которых электроны меняют хозяина
+ Горение, ржавление, дыхание, работа батарейки — всё это окислительно-восстановительные реакции. В них одни атомы отдают электроны, другие принимают, и степени окисления меняются.
+
+
-
-
- Содержание раздела
+
+
+
+
+
+
+
-
- § 42 Степень окисления
- § 43 Процессы окисления и восстановления
- § 44 Окислительно-восстановительные реакции
- § 45 Окислительно-восстановительные реакции вокруг нас
-
+
-
+
+