From 39515af6bf061f4ac75184004f0af95fe11f1f59 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 14:36:31 +0300 Subject: [PATCH] =?UTF-8?q?@=20feat(chemistry-8):=20Phase=201=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=C2=AB=D0=9A=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82=D0=B8=D1=8F=C2=BB?= =?UTF-8?q?=20(=C2=A71=E2=80=939=20+=20=D0=9F=D0=A01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Полноценная интерактивная страница chemistry_8_intro.html (9 § + ПР1 + босс): - §1 карта элементов (Z, название, Ar), §2 калькулятор Mr по формуле - §3 «порция вещества» n⇒N,m, §4 счётчик частиц N=n·N_A, §5 M+молярный объём - §6 звёздный виджет: интерактивный треугольник n–m–M - §7 универсальный калькулятор газа (m–n–V–N), §8 балансировщик уравнений - §9 пошаговый решатель по уравнению; босс раздела (4 задачи) + ачивка «Счёт в химии» - прогресс/XP через /api/textbooks/chemistry-8-intro/progress, scrollspy, тема chem8_svg.js: реализованы движки — molarMass (школьные Ar: Mr(H2O)=18), elementCounts, moleTriangle, equationBalancer (+ fmt, arOf). Фикс порядка загрузки: инициализация обёрнута в DOMContentLoaded (defer-скрипты готовы к этому моменту). Генератор каркасов получил skip-if-exists (--force для перезаписи). Тесты: chemistry8.test.js (14) + chemistry8-dom.test.js (jsdom-смоук виджетов, 3) — 17/17. Co-Authored-By: Claude Opus 4.8 (1M context) @ --- backend/scripts/gen_chem8_skeletons.js | 16 +- backend/tests/chemistry8-dom.test.js | 63 ++ backend/tests/chemistry8.test.js | 42 +- frontend/js/chem8_svg.js | 259 ++++++- frontend/textbooks/chemistry_8_intro.html | 778 ++++++++++++++++++++-- 5 files changed, 1082 insertions(+), 76 deletions(-) create mode 100644 backend/tests/chemistry8-dom.test.js diff --git a/backend/scripts/gen_chem8_skeletons.js b/backend/scripts/gen_chem8_skeletons.js index 4088a6b..5161e94 100644 --- a/backend/scripts/gen_chem8_skeletons.js +++ b/backend/scripts/gen_chem8_skeletons.js @@ -287,11 +287,19 @@ const _TB_SLUG = '${ch.slug}'; `; } -let count = 0; +// --force перезапишет уже существующие файлы; по умолчанию — пропускаем +// готовые (наполненные в фазах) страницы, чтобы не затереть контент. +const FORCE = process.argv.includes('--force'); +let count = 0, skipped = 0; for (const ch of CHAPTERS) { - const html = pageHtml(ch); - fs.writeFileSync(path.join(OUT, ch.file), html, 'utf8'); + const target = path.join(OUT, ch.file); + if (!FORCE && fs.existsSync(target)) { + skipped++; + console.log('skip ', ch.file, '(уже существует — наполнен в фазе)'); + continue; + } + fs.writeFileSync(target, pageHtml(ch), 'utf8'); count++; console.log('written', ch.file, '(' + ch.items.filter(i => i.t).length + ' §)'); } -console.log('done:', count, 'chapter skeletons'); +console.log('done:', count, 'written,', skipped, 'skipped'); diff --git a/backend/tests/chemistry8-dom.test.js b/backend/tests/chemistry8-dom.test.js new file mode 100644 index 0000000..e4a21e4 --- /dev/null +++ b/backend/tests/chemistry8-dom.test.js @@ -0,0 +1,63 @@ +'use strict'; +/* + * jsdom-смоук виджетов chem8_svg.js: реальная отрисовка в DOM, ввод, проверка. + * Ловит рантайм-ошибки DOM-манипуляций, которые не видны в чистых юнит-тестах. + */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const { JSDOM } = require('jsdom'); + +const SRC = fs.readFileSync( + path.join(__dirname, '..', '..', 'frontend', 'js', 'chem8_svg.js'), 'utf8'); + +function mkDom() { + const dom = new JSDOM('
'); + // выполняем модуль так, что его `window` === jsdom-окно + new Function('window', SRC)(dom.window); + return { dom, C: dom.window.Chem8, doc: dom.window.document }; +} + +function fire(el, type) { + el.dispatchEvent(new el.ownerDocument.defaultView.Event(type, { bubbles: true })); +} + +test('moleTriangle монтируется и считает m = n·M', () => { + const { C, doc } = mkDom(); + const api = C.moleTriangle(doc.getElementById('m'), {}); + assert.ok(api && api.el, 'виджет смонтирован'); + const inputs = doc.querySelectorAll('#m input[data-k]'); + assert.equal(inputs.length, 3, '3 поля'); + const byKey = {}; + inputs.forEach(i => { byKey[i.getAttribute('data-k')] = i; }); + // вводим n=2, затем M=18 → ожидаем m=36 + byKey.n.value = '2'; fire(byKey.n, 'input'); + byKey.M.value = '18'; fire(byKey.M, 'input'); + const out = doc.querySelector('#m [data-out]'); + assert.ok(/36/.test(out.textContent), 'm = 36 вычислено: ' + out.textContent); +}); + +test('equationBalancer: неверные коэффициенты → дисбаланс, верные → баланс', () => { + const { C, doc } = mkDom(); + const api = C.equationBalancer(doc.getElementById('b'), + { skeleton: 'H2 + O2 -> H2O', solution: [2, 1, 2] }); + assert.ok(api && api.check, 'виджет смонтирован'); + // по умолчанию все коэффициенты = 1 → не сбалансировано + assert.equal(api.check(), false, '1·H2 + 1·O2 -> 1·H2O не сбалансировано'); + const out = doc.querySelector('#b [data-out]'); + assert.ok(out.className.includes('bad'), 'подсветка дисбаланса'); + // применяем решение через кнопку + doc.querySelector('#b [data-solve]').dispatchEvent( + new doc.defaultView.Event('click', { bubbles: true })); + assert.ok(out.className.includes('ok'), 'после решения — сбалансировано: ' + out.className); +}); + +test('equationBalancer считает атомы для сложной реакции', () => { + const { C, doc } = mkDom(); + const api = C.equationBalancer(doc.getElementById('b'), + { skeleton: 'Al + HCl -> AlCl3 + H2', solution: [2, 6, 2, 3] }); + const coefs = doc.querySelectorAll('#b .ceqb-coef'); + [2, 6, 2, 3].forEach((v, i) => { coefs[i].value = String(v); }); + assert.equal(api.check(), true, '2Al + 6HCl -> 2AlCl3 + 3H2 сбалансировано'); +}); diff --git a/backend/tests/chemistry8.test.js b/backend/tests/chemistry8.test.js index ab21428..f9fd2bf 100644 --- a/backend/tests/chemistry8.test.js +++ b/backend/tests/chemistry8.test.js @@ -49,13 +49,34 @@ test('Chem8.chemEq — обратимая реакция и осадок', () => assert.ok(prec.includes('AgCl↓'), 'значок осадка'); }); +test('Chem8.molarMass — школьные Ar (Mr из учебника)', () => { + assert.equal(C.molarMass('H2O'), 18); + assert.equal(C.molarMass('CaCO3'), 100); + assert.equal(C.molarMass('H2SO4'), 98); + assert.equal(C.molarMass('Al2(SO4)3'), 342); + assert.equal(C.molarMass('NaOH'), 40); + assert.ok(Number.isNaN(C.molarMass('Xx9')), 'неизвестный элемент → NaN'); +}); + +test('Chem8.elementCounts — скобки и индексы', () => { + assert.deepEqual(C.elementCounts('Ca(OH)2'), { Ca: 1, O: 2, H: 2 }); + assert.deepEqual(C.elementCounts('Al2(SO4)3'), { Al: 2, S: 3, O: 12 }); + assert.deepEqual(C.elementCounts('CO2'), { C: 1, O: 2 }); +}); + test('Chem8 — заглушки возвращают null и не падают', () => { - for (const fn of ['testTube', 'moleTriangle', 'solubilityTable', 'oxStateCalc', 'geneticMap']) { + for (const fn of ['testTube', 'solubilityTable', 'oxStateCalc', 'geneticMap']) { assert.equal(typeof C[fn], 'function', fn + ' определён'); assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null'); } }); +test('Chem8 — движки расчётов экспортированы как функции', () => { + for (const fn of ['moleTriangle', 'equationBalancer']) { + assert.equal(typeof C[fn], 'function', fn + ' определён'); + } +}); + // --- каркас страниц --- const CHILDREN = [ { slug: 'chemistry-8-intro', file: 'chemistry_8_intro.html', paras: 9 }, @@ -90,6 +111,25 @@ test('каждая глава существует и задаёт свой _TB_ } }); +test('Phase 1 — раздел intro наполнен (9 § + ПР1 + босс)', () => { + const html = fs.readFileSync(path.join(TB, 'chemistry_8_intro.html'), 'utf8'); + for (let i = 1; i <= 9; i++) assert.ok(html.includes('id="p' + i + '"'), '§' + i + ' секция'); + assert.ok(html.includes('id="pr1"'), 'ПР1'); + assert.ok(html.includes('id="boss"'), 'босс раздела'); + assert.ok(html.includes('id="mt-mount"'), 'треугольник n–m–M'); + assert.ok(html.includes('id="bal-mount"'), 'балансировщик'); + assert.ok(html.includes("READ_IDS = ['p1','p2','p3','p4','p5','p6','p7','p8','p9']"), '9 читаемых § для прогресса'); + assert.ok(!html.includes('Раздел в разработке'), 'баннер-заглушка убран'); +}); + +test('Phase 1 — ответы босса согласованы с molarMass', () => { + // значения в боссе intro должны совпадать с движком + assert.equal(C.molarMass('H2SO4'), 98); // задача 1 + assert.equal(C.molarMass('NaOH'), 40); // задача 2 (M в условии) + assert.ok(Math.abs(3 * 22.4 - 67.2) < 1e-9); // задача 3: V=n·Vm + assert.ok(Math.abs(2 * 6.02 - 12.04) < 1e-9); // задача 4: N=n·N_A +}); + test('миграция 041 — родитель chemistry-8 + 7 детей, нет эмоджи', () => { const sql = fs.readFileSync( path.join(ROOT, 'backend', 'src', 'db', 'migrations', '041_chemistry8_hub.sql'), 'utf8'); diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js index 3f544f2..9a411c3 100644 --- a/frontend/js/chem8_svg.js +++ b/frontend/js/chem8_svg.js @@ -102,6 +102,254 @@ }); } + /* ── Относительные атомные массы Ar (школьно-округлённые, как в учебнике РБ). + Намеренно НЕ берём точные массы biochem-core: для 8 класса Mr(H₂O)=18, + Mr(CaCO₃)=100 и т. п. — иначе расходимся с ответами учебника. ── */ + var AR = { + H:1, He:4, Li:7, Be:9, B:11, C:12, N:14, O:16, F:19, Ne:20, + Na:23, Mg:24, Al:27, Si:28, P:31, S:32, Cl:35.5, Ar:40, K:39, Ca:40, + Sc:45, Ti:48, V:51, Cr:52, Mn:55, Fe:56, Co:59, Ni:59, Cu:64, Zn:65, + Ga:70, Ge:73, As:75, Se:79, Br:80, Kr:84, Rb:85, Sr:88, Ag:108, Cd:112, + Sn:119, Sb:122, I:127, Xe:131, Ba:137, Pt:195, Au:197, Hg:201, Pb:207, Bi:209 + }; + function arOf(sym) { + if (Object.prototype.hasOwnProperty.call(AR, sym)) return AR[sym]; + // запасной путь — точная масса из biochem-core, если элемента нет в школьной таблице + if (global.BIO && global.BIO.ELEMENTS && global.BIO.ELEMENTS[sym]) { + return Math.round(global.BIO.ELEMENTS[sym].mass); + } + return 0; + } + + /* elementCounts('Ca(OH)2') -> {Ca:1, O:2, H:2} (скобки и индексы) */ + function elementCounts(str) { + var out = {}, stack = [out]; + var re = /([A-Z][a-z]?)(\d*)|(\()|(\))(\d*)/g, m; + while ((m = re.exec(str)) !== null) { + if (m[1]) { + var n = m[2] ? parseInt(m[2], 10) : 1; + var top = stack[stack.length - 1]; + top[m[1]] = (top[m[1]] || 0) + n; + } else if (m[3]) { + stack.push({}); + } else if (m[4] !== undefined) { + var grp = stack.pop(), mult = m[5] ? parseInt(m[5], 10) : 1, t2 = stack[stack.length - 1]; + for (var k in grp) t2[k] = (t2[k] || 0) + grp[k] * mult; + } + } + return out; + } + + /* molarMass('CaCO3') -> 100 (г/моль), на школьных Ar. NaN при неизвестном элементе. */ + function molarMass(str) { + var c = elementCounts(String(str || '').replace(/\s+/g, '')); + var keys = Object.keys(c); + if (!keys.length) return NaN; + var m = 0; + for (var i = 0; i < keys.length; i++) { + var a = arOf(keys[i]); + if (!a) return NaN; + m += a * c[keys[i]]; + } + return Math.round(m * 1000) / 1000; + } + + /* Округление до значащих для вывода (избегаем 18.000000002). */ + function fmt(x, d) { + if (!isFinite(x)) return '—'; + var p = Math.pow(10, d == null ? 3 : d); + return String(Math.round(x * p) / p); + } + + /* ────────────────────────────────────────────────────────────────────────── + moleTriangle(mount, opts) — интерактивный калькулятор-треугольник n–m–M. + Пользователь вводит любые два из {n, m, M} — третье считается (n=m/M, + m=n·M, M=m/n). opts.substance — предзаполнить M по формуле (через molarMass). + Возвращает {el, get, set}. Без setPointerCapture, чистый DOM. + ────────────────────────────────────────────────────────────────────────── */ + function moleTriangle(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + opts = opts || {}; + var state = { n: '', m: '', M: opts.substance ? molarMass(opts.substance) : '' }; + var lastEdited = []; // последние два редактированных поля → третье вычисляем + + host.innerHTML = + '
' + + '' + + '
' + + fieldHtml('n', 'n, моль', 'химическое количество') + + fieldHtml('m', 'm, г', 'масса вещества') + + fieldHtml('M', 'M, г/моль', 'молярная масса') + + '
' + + '
Введите любые два значения — третье вычислится.
' + + '
'; + + function fieldHtml(key, label, hint) { + return ''; + } + + var inputs = host.querySelectorAll('input[data-k]'); + var out = host.querySelector('[data-out]'); + + function num(v) { var x = parseFloat(String(v).replace(',', '.')); return isFinite(x) ? x : null; } + + function recompute(changedKey) { + if (lastEdited[0] !== changedKey) { lastEdited.unshift(changedKey); lastEdited = lastEdited.slice(0, 2); } + var known = ['n', 'm', 'M'].filter(function (k) { return num(state[k]) !== null; }); + // целевое поле — то, что НЕ редактировали последним и пусто/производно + var target = ['n', 'm', 'M'].filter(function (k) { return lastEdited.indexOf(k) === -1; })[0]; + if (!target) return; + var n = num(state.n), m = num(state.m), M = num(state.M); + var res = null, formula = ''; + if (target === 'n' && m !== null && M) { res = m / M; formula = 'n = m / M = ' + fmt(m) + ' / ' + fmt(M); } + else if (target === 'm' && n !== null && M !== null) { res = n * M; formula = 'm = n · M = ' + fmt(n) + ' · ' + fmt(M); } + else if (target === 'M' && m !== null && n) { res = m / n; formula = 'M = m / n = ' + fmt(m) + ' / ' + fmt(n); } + if (res === null) { + out.className = 'mtri-out'; + out.textContent = (known.length >= 2) + ? 'Проверьте: на ноль делить нельзя.' + : 'Введите любые два значения — третье вычислится.'; + return; + } + var unit = target === 'n' ? ' моль' : target === 'm' ? ' г' : ' г/моль'; + setField(target, fmt(res)); + out.className = 'mtri-out ok'; + out.innerHTML = '' + target + ' = ' + fmt(res) + unit + '' + formula + ''; + } + + function setField(key, val) { + state[key] = val; + for (var i = 0; i < inputs.length; i++) { + if (inputs[i].getAttribute('data-k') === key && global.document.activeElement !== inputs[i]) { + inputs[i].value = val; + } + } + } + + for (var i = 0; i < inputs.length; i++) { + (function (inp) { + inp.addEventListener('input', function () { + var k = inp.getAttribute('data-k'); + state[k] = inp.value; + // если поле очистили — сбросить производное + recompute(k); + }); + })(inputs[i]); + } + + if (state.M) setField('M', fmt(state.M)); + + return { + el: host, + get: function () { return { n: num(state.n), m: num(state.m), M: num(state.M) }; }, + set: function (k, v) { setField(k, String(v)); recompute(k === 'n' ? 'm' : 'n'); } + }; + } + + /* ────────────────────────────────────────────────────────────────────────── + equationBalancer(mount, {skeleton}) — проверка расстановки коэффициентов. + skeleton: 'H2 + O2 -> H2O'. Рендерит поля коэффициентов перед каждым + веществом, кнопку «Проверить»; считает баланс атомов по сторонам и + подсвечивает несбалансированные элементы. opts.solution — массив верных + коэффициентов (для кнопки «Показать решение»). + ────────────────────────────────────────────────────────────────────────── */ + function equationBalancer(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + opts = opts || {}; + var skel = String(opts.skeleton || ''); + var sides = skel.split(/->|=|→/); + var left = parseSide(sides[0] || ''), right = parseSide(sides[1] || ''); + var all = left.concat(right); + + host.innerHTML = + '
' + + '
' + + renderSpecies(left) + '' + renderSpecies(right) + + '
' + + '
' + + '' + + (opts.solution ? '' : '') + + '' + + '
' + + '
' + + '
'; + + function renderSpecies(list) { + return list.map(function (sp, i) { + var gi = all.indexOf(sp); + return (i ? '+' : '') + + '' + formula(sp.raw) + ''; + }).join(''); + } + + var out = host.querySelector('[data-out]'); + var coefs = host.querySelectorAll('.ceqb-coef'); + + function getCoef(i) { var v = parseInt((coefs[i] && coefs[i].value) || '1', 10); return v > 0 ? v : 1; } + + function tally(list, fromIdx) { + var acc = {}; + list.forEach(function (sp, j) { + var c = getCoef(all.indexOf(sp)); + for (var e in sp.counts) acc[e] = (acc[e] || 0) + sp.counts[e] * c; + }); + return acc; + } + + function check() { + var L = tally(left), R = tally(right); + var elems = {}; Object.keys(L).forEach(function (e) { elems[e] = 1; }); Object.keys(R).forEach(function (e) { elems[e] = 1; }); + var rows = '', ok = true; + Object.keys(elems).sort().forEach(function (e) { + var l = L[e] || 0, r = R[e] || 0, eq = l === r; + if (!eq) ok = false; + rows += '' + e + '' + l + '' + r + '' + + '' + (eq ? '✓' : '≠') + ''; + }); + out.className = 'ceqb-out ' + (ok ? 'ok' : 'bad'); + out.innerHTML = (ok ? '
Уравнение сбалансировано.
' + : '
Не сходится — выровняйте выделенные элементы.
') + + '' + + rows + '
ЭлементСлеваСправа
'; + return ok; + } + + var btnCheck = host.querySelector('[data-check]'); + var btnSolve = host.querySelector('[data-solve]'); + var btnReset = host.querySelector('[data-reset]'); + if (btnCheck) btnCheck.addEventListener('click', check); + if (btnReset) btnReset.addEventListener('click', function () { + for (var i = 0; i < coefs.length; i++) coefs[i].value = '1'; + out.className = 'ceqb-out'; out.innerHTML = ''; + }); + if (btnSolve && opts.solution) btnSolve.addEventListener('click', function () { + for (var i = 0; i < coefs.length && i < opts.solution.length; i++) coefs[i].value = String(opts.solution[i]); + check(); + }); + + return { el: host, check: check }; + } + + /* 'H2 + O2' -> [{raw:'H2', counts:{H:2}}, {raw:'O2', counts:{O:2}}] */ + function parseSide(side) { + return String(side).split('+').map(function (t) { return t.trim(); }).filter(Boolean) + .map(function (raw) { + var r = raw.replace(/^\d+/, '').trim(); // коэффициент в скелете игнорируем + return { raw: r, counts: elementCounts(r) }; + }); + } + /* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */ function notImplemented(name) { return function () { @@ -119,10 +367,15 @@ chemEq: chemEq, toSub: toSub, toSup: toSup, - // заглушки (см. план, разд. B) — наполняются в Phase 1–6 + // готово (Phase 1 — движки расчётов) + elementCounts: elementCounts, + molarMass: molarMass, // school-rounded Ar: Mr(H2O)=18 + arOf: arOf, + fmt: fmt, + moleTriangle: moleTriangle, // §6 — треугольник n–m–M + equationBalancer: equationBalancer, // §8 — балансировщик уравнений + // заглушки (см. план, разд. B) — наполняются в Phase 2–6 testTube: notImplemented('testTube'), // §18,25 — пробирка: осадок/газ/окраска - moleTriangle: notImplemented('moleTriangle'), // §6 — треугольник n–m–M - equationBalancer: notImplemented('equationBalancer'),// §8 — балансировщик уравнений oxStateCalc: notImplemented('oxStateCalc'), // §42 — калькулятор степени окисления redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма diff --git a/frontend/textbooks/chemistry_8_intro.html b/frontend/textbooks/chemistry_8_intro.html index be31a0d..671c02b 100644 --- a/frontend/textbooks/chemistry_8_intro.html +++ b/frontend/textbooks/chemistry_8_intro.html @@ -7,61 +7,200 @@ Химия 8 · Вводный раздел · «Количественные понятия в химии» - + - + @@ -73,7 +212,7 @@ html.dark .ol-note span:last-child{color:var(--pri-l)} К разделам
-
Вводный раздел · § 1–9
+
Вводный раздел · § 1–9 · ПР 1

Количественные понятия в химии

@@ -85,42 +224,263 @@ html.dark .ol-note span:last-child{color:var(--pri-l)}
-
-
-
- +
+
+
n
+
+
Прогресс раздела
+
0 из 9 параграфов · 0%
+
-
-

Раздел в разработке

-

Интерактивное наглядное наполнение этого раздела (теория, модели, симуляторы, тренажёры и боссы) добавляется поэтапно. Ниже — план параграфов раздела согласно учебнику.

-
-
- -
- - Содержание раздела +
0 XP
-
    -
  • § 1Атомы. Химические элементы. Относительная атомная масса
  • -
  • § 2Молекулы. Простые и сложные вещества. Химические формулы. Относительная молекулярная масса
  • -
  • § 3Химическое количество вещества
  • -
  • § 4Моль — единица химического количества вещества. Постоянная Авогадро
  • -
  • § 5Молярная масса. Молярный объём газов
  • -
  • § 6Вычисление химического количества вещества по его массе и массы вещества по его химическому количеству
  • -
  • § 7Вычисление химического количества газа по его объёму и объёма газа по его химическому количеству
  • -
  • Практическая работа 1. Химическое количество вещества
  • -
  • § 8Химические реакции
  • -
  • § 9Количественные расчёты по уравнениям химических реакций
  • -
-
+ -
- Интерактивный учебник «Химия — 8 класс» · Вводный раздел · LearnSpace -
+
+ + +
+ +
+
§ 1

Атомы. Химические элементы. Относительная атомная масса

$A_r(\text{O}) = 16$
+
+

теория Атом и химический элемент

+

Атом — мельчайшая химически неделимая частица вещества. Химический элемент — вид атомов с одинаковым зарядом ядра. Каждый элемент имеет символ (например, H, O, Fe) и порядковый номер $Z$ в периодической системе.

+
Относительная атомная масса $A_r$ показывает, во сколько раз масса атома больше $\tfrac{1}{12}$ массы атома углерода-12. Величина безразмерная: $A_r(\text{H})=1$, $A_r(\text{O})=16$, $A_r(\text{Fe})=56$.
+
+
+
Карта элементов: клик → $Z$, название, $A_r$
+
+
Выберите элемент, чтобы увидеть его характеристики.
+
+
+
Во сколько раз атом серы ($A_r=32$) тяжелее атома кислорода ($A_r=16$)?
+
+
+
+
Изучено
+
+ +
+
§ 2

Молекулы. Простые и сложные вещества. Химические формулы. $M_r$

$M_r=\sum A_r$
+
+

теория Вещества и формулы

+

Простое вещество образовано атомами одного элемента ($\text{O}_2$, $\text{Fe}$), сложное — разных ($\text{H}_2\text{O}$, $\text{CaCO}_3$). Химическая формула показывает качественный и количественный состав: индекс — число атомов элемента.

+
Относительная молекулярная масса $M_r$ равна сумме относительных атомных масс всех атомов в формуле. Например, $M_r(\text{H}_2\text{O}) = 2\cdot1 + 16 = 18$.
+
+
+
Калькулятор $M_r$ по формуле
+
+ + + +
+
+ + + + +
+
Введите формулу и нажмите «Вычислить».
+
+
Изучено
+
+ +
+
§ 3

Химическое количество вещества

$n$, моль
+
+

теория Порция вещества

+

Считать атомы и молекулы поштучно невозможно — их слишком много. Поэтому ввели специальную «порцию» — химическое количество вещества $n$, единица — моль. Одна и та же порция ($1$ моль) любого вещества содержит одинаковое число частиц.

+
Химическое количество $n$ связывает массу $m$, число частиц $N$ и объём газа $V$. Это «мост» между миром атомов и граммами на весах.
+
+
+
Порция вещества: $n \Rightarrow N$ и $m$
+
+ + + + + 1,0 +
+
+
+
Изучено
+
+ +
+
§ 4

Моль — единица химического количества. Постоянная Авогадро

$N = n\cdot N_A$
+
+

теория Постоянная Авогадро

+
1 моль — это химическое количество вещества, содержащее столько же частиц, сколько атомов в $12$ г углерода-$12$, а именно $N_A = 6{,}02\cdot10^{23}$ частиц/моль — постоянная Авогадро.
+

Число частиц: $N = n\cdot N_A$. Отсюда $n = \dfrac{N}{N_A}$.

+
+
+
Счётчик частиц $N = n\cdot N_A$
+
2,0
+
+
+
+
Сколько молекул содержится в $0{,}5$ моль воды? ($N_A=6{,}02\cdot10^{23}$)
+
+
+
+
Изучено
+
+ +
+
§ 5

Молярная масса. Молярный объём газов

$V_m=22{,}4$ л/моль
+
+

теория M и Vm

+
Молярная масса $M$ — масса $1$ моль вещества (г/моль). Численно $M$ равна $M_r$: $M(\text{H}_2\text{O})=18$ г/моль.
+
Молярный объём $V_m$ — объём $1$ моль газа. При нормальных условиях (н.у.) $V_m = 22{,}4$ л/моль для любого газа (закон Авогадро).
+
+
+
M по формуле и объём 1 моль газа
+
+
M(CO₂) и объём при н.у. появятся здесь.
+
+
Изучено
+
+ +
+
§ 6 · звёздный виджет

Вычисление $n$ по массе и массы по $n$

$n = \dfrac{m}{M}$
+
+

правило Треугольник n–m–M

+

Три величины связаны формулой $m = n\cdot M$. Закрой искомую — получишь формулу: $n=\dfrac{m}{M}$, $m=n\cdot M$, $M=\dfrac{m}{n}$.

+
+
Дано: $m=36$ г воды, $M=18$ г/моль.
+
$n = \dfrac{m}{M} = \dfrac{36}{18} = 2$ моль.
+
+
+
+
Интерактивный треугольник n–m–M
+
+
+
+
Изучено
+
+ +
+
§ 7

Вычисление $n$ газа по объёму и объёма по $n$

$n = \dfrac{V}{V_m}$
+
+

правило Связка m – n – V – N

+

Для газа при н.у.: $n=\dfrac{V}{V_m}$, $V=n\cdot V_m$ ($V_m=22{,}4$ л/моль). Вместе с $n=\dfrac{m}{M}$ и $N=n\cdot N_A$ это единая система: зная одно — найдёшь всё.

+
+
+
Универсальный калькулятор газа
+
+
+ + + +
+
+
+
Изучено
+
+ +
+
Практическая работа 1

Химическое количество вещества

$n=\dfrac{m}{M}$
+
+

практика Порядок работы

+
    +
  • Взвесь на весах образцы веществ (например, $\text{NaCl}$, $\text{CuSO}_4$).
  • +
  • Запиши массу $m$ и определи молярную массу $M$ по формуле.
  • +
  • Вычисли химическое количество $n=\dfrac{m}{M}$ и число частиц $N=n\cdot N_A$.
  • +
  • Оформи вывод: какому числу частиц соответствует взятая масса.
  • +
+
Работай аккуратно с реактивами и весами; не пробуй вещества на вкус.
+
+
Выполнено
+
+ +
+
§ 8 · звёздный виджет

Химические реакции

закон сохранения массы
+
+

теория Уравнение реакции

+

В химической реакции одни вещества превращаются в другие, но атомы не исчезают и не появляются (закон сохранения массы М. В. Ломоносова, А. Лавуазье). Поэтому уравнение реакции уравнивают коэффициентами — число атомов каждого элемента слева и справа равно.

+

Типы реакций: соединения ($A+B\to AB$), разложения ($AB\to A+B$), замещения ($A+BC\to AC+B$), обмена ($AB+CD\to AD+CB$).

+
+
+
Балансировщик: расставь коэффициенты
+
+ +
+
+
+
Изучено
+
+ +
+
§ 9 · звёздный виджет

Количественные расчёты по уравнениям реакций

по мольным отношениям
+
+

правило Алгоритм расчёта

+
    +
  • Записать и уравнять уравнение реакции.
  • +
  • Найти $n$ известного вещества: $n=\dfrac{m}{M}$ (или $\dfrac{V}{V_m}$).
  • +
  • По коэффициентам найти $n$ искомого (мольное отношение).
  • +
  • Перейти к массе/объёму: $m=n\cdot M$ ($V=n\cdot V_m$).
  • +
+
+
+
Пошаговый решатель по уравнению
+
+
+
+
+
Изучено
+
+ +
+
+
Босс раздела: количественные понятия
+
4 задачи на всё, что изучено. За каждую — +10 XP. Победишь всех — ачивка «Счёт в химии» и +30 XP.
+
Решено: 0 / 4
+
+
+ + Вводный раздел пройден! Ачивка «Счёт в химии» получена. + К разделам → +
+
+
+ +
+
+ + +
Интерактивный учебник «Химия — 8 класс» · Вводный раздел · LearnSpace