From 787092674a27c02572fee99673c1a470b80e5184 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 15:20:13 +0300 Subject: [PATCH] =?UTF-8?q?@=20feat(chemistry-8):=20Phase=202=20=E2=80=94?= =?UTF-8?q?=20=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=201=20=C2=AB=D0=92=D0=B0?= =?UTF-8?q?=D0=B6=D0=BD=D0=B5=D0=B9=D1=88=D0=B8=D0=B5=20=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D1=8B=20=D0=BD=D0=B5=D0=BE=D1=80=D0=B3.=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=C2=BB=20?= =?UTF-8?q?(=C2=A710=E2=80=9323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Полная глава на движке (14 § + 2 лаб. опыта + 2 практические работы + финал-босс): - §10–12 оксиды (классификатор, свойства, получение) - §13–15 кислоты (классификатор, ряд активности, индикаторы, получение) - §16–18 основания (классификатор, фенолфталеин, Лаб.1 Cu(OH)₂↓, ПР2 нейтрализация) - §19–21 соли (таблица растворимости, РИО, соль+металл, Лаб.2, способы) - §22 генетическая связь классов + ПР3; §23 расчётный решатель; финал-босс (6 задач) - POOLS: ~45 задач (MCQ + числовые), шпаргалки и подсказки по каждому § chem8_svg.js: реализованы 5 хим-виджетов (были заглушки) — testTube (осадок/газ), indicatorScale (лакмус/фенолфталеин/метилоранж + pH), classifier (клик-DnD), solubilityTable (катион×анион), activitySeries (ряд активности металлов). chem8-textbook.css: стили виджетов. chem8_ch1_widgets.js: монтаж по §. Тесты: 24/24 (юнит + jsdom-виджеты + полностраничный SPA intro и ch1 — para-selector, активный §, монтаж флагманов, тренажёр, без ошибок). Ассеты 200. Co-Authored-By: Claude Opus 4.8 (1M context) @ --- backend/tests/chemistry8-page.test.js | 80 ++--- backend/tests/chemistry8.test.js | 32 +- frontend/css/chem8-textbook.css | 45 +++ frontend/js/chem8_ch1_widgets.js | 103 ++++++ frontend/js/chem8_svg.js | 220 +++++++++++- frontend/textbooks/chemistry_8_ch1.html | 423 ++++++++++++++++++------ 6 files changed, 740 insertions(+), 163 deletions(-) create mode 100644 frontend/js/chem8_ch1_widgets.js diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js index c92b342..09eddc6 100644 --- a/backend/tests/chemistry8-page.test.js +++ b/backend/tests/chemistry8-page.test.js @@ -1,8 +1,8 @@ 'use strict'; /* - * Полностраничная jsdom-проверка chemistry_8_intro.html (SPA на chem8_engine.js): - * выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем, - * что para-selector построен, первый § активен и виджеты смонтированы — без ошибок. + * Полностраничная jsdom-проверка глав «Химия 8» (SPA на chem8_engine.js): + * выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем + * para-selector, активный §, монтаж виджетов — без ошибок скриптов. */ const test = require('node:test'); const assert = require('node:assert'); @@ -14,70 +14,70 @@ const ROOT = path.join(__dirname, '..', '..'); const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8'); const wait = ms => new Promise(r => setTimeout(r, ms)); -function buildPage() { - let html = readF('frontend/textbooks/chemistry_8_intro.html'); +function buildPage(file, widgetsSrc) { + let html = readF('frontend/textbooks/' + file); const inl = { '/js/biochem-core.js': readF('frontend/js/biochem-core.js'), '/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'), - '/js/chem8_intro_widgets.js': readF('frontend/js/chem8_intro_widgets.js'), + [widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/js', '')), '/js/chem8_engine.js': readF('frontend/js/chem8_engine.js') }; - // CDN katex → удалить; api/xp → стабы (LS отсутствует, renderMathInElement — no-op) html = html .replace(/') .replace(/'); }); return html; } -async function loadDom() { +async function loadDom(file, widgetsSrc) { const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message)); - const dom = new JSDOM(buildPage(), { + const dom = new JSDOM(buildPage(file, widgetsSrc), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', - beforeParse(w) { w.scrollTo = function () {}; } // jsdom не реализует scrollTo (в браузере есть) + beforeParse(w) { w.scrollTo = function () {}; } }); - await wait(180); // дать отработать таймерам сборки § и монтажа виджетов (40–50 мс) + await wait(180); return { dom, errors, doc: dom.window.document }; } -test('страница SPA выполняется без ошибок скриптов', async () => { - const { errors } = await loadDom(); - assert.deepEqual(errors, [], 'нет jsdomError: ' + errors.join(' | ')); -}); - -test('para-selector построен (11 карточек) и первый § активен', async () => { - const { doc } = await loadDom(); - assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 11, '11 карточек §'); - const active = doc.querySelector('.sec.active'); - assert.ok(active && active.id === 'sec-p1', 'активен §1'); - assert.ok(doc.querySelector('#p1-body .para-hero'), 'para-hero §1 построен'); -}); - -test('виджеты § смонтированы движком', async () => { - const { doc } = await loadDom(); - assert.ok(doc.querySelectorAll('#p1-el .el-cell').length > 10, 'карта элементов §1'); - // перейдём на §6 и §8 через goTo, дождёмся монтажа флагманов +/* ── Вводный раздел ── */ +test('intro: SPA без ошибок, 11 карточек, §1 активен, виджеты', async () => { + const { doc, errors } = await loadDom('chemistry_8_intro.html', '/js/chem8_intro_widgets.js'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); + assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 11, '11 карточек'); + assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p1', '§1 активен'); + assert.ok(doc.querySelectorAll('#p1-el .el-cell').length > 10, 'карта элементов'); doc.defaultView.goTo('p6'); await wait(120); assert.ok(doc.querySelector('#p6-mount .mtri'), 'треугольник §6'); - doc.defaultView.goTo('p8'); await wait(120); - assert.ok(doc.querySelector('#p8-mount .ceqb'), 'балансировщик §8'); }); -test('тренажёр задач отрисован для §2 (POOLS)', async () => { - const { doc } = await loadDom(); - doc.defaultView.goTo('p2'); await wait(150); - assert.ok(doc.querySelector('#taskArea p2, #taskAreap2'), 'область задач §2'); - assert.ok(doc.querySelectorAll('#navDotsp2 .nav-dot').length >= 4, 'навигация по задачам §2'); +/* ── Глава 1 ── */ +test('ch1: SPA без ошибок, 15 карточек, §10 активен', async () => { + const { doc, errors } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); + assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 15, '14 § + финал'); + assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p10', '§10 активен'); + assert.ok(doc.querySelector('#p10-body .para-hero'), 'para-hero §10'); }); -test('Chem8 доступен и считает Mr', async () => { - const { dom } = await loadDom(); - assert.ok(dom.window.Chem8, 'window.Chem8 определён'); - assert.equal(dom.window.Chem8.molarMass('CaCO3'), 100); +test('ch1: флагман-виджеты монтируются (классификатор, растворимость, ряд активности)', async () => { + const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js'); + doc.defaultView.goTo('p10'); await wait(120); + assert.ok(doc.querySelector('#c-ox-cls .cls-chip'), 'классификатор оксидов §10'); + doc.defaultView.goTo('p13'); await wait(120); + assert.ok(doc.querySelector('#c-acid-ind .ind-strip'), 'индикатор §13'); + doc.defaultView.goTo('p19'); await wait(120); + assert.ok(doc.querySelector('#c-salt-sol .sol-tab'), 'таблица растворимости §19'); + doc.defaultView.goTo('p14'); await wait(120); + assert.ok(doc.querySelector('#c-acid-act .act-cell'), 'ряд активности §14'); +}); + +test('ch1: тренажёр задач отрисован для §10', async () => { + const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js'); + await wait(150); + assert.ok(doc.querySelectorAll('#navDotsp10 .nav-dot').length >= 4, 'навигация по задачам §10'); }); diff --git a/backend/tests/chemistry8.test.js b/backend/tests/chemistry8.test.js index 52fb399..e37c33e 100644 --- a/backend/tests/chemistry8.test.js +++ b/backend/tests/chemistry8.test.js @@ -64,13 +64,20 @@ test('Chem8.elementCounts — скобки и индексы', () => { assert.deepEqual(C.elementCounts('CO2'), { C: 1, O: 2 }); }); -test('Chem8 — заглушки возвращают null и не падают', () => { - for (const fn of ['testTube', 'solubilityTable', 'oxStateCalc', 'geneticMap']) { +test('Chem8 — оставшиеся заглушки возвращают null и не падают', () => { + for (const fn of ['oxStateCalc', 'redoxBalancer', 'orbitalDiagram', 'miniPeriodic', 'dissociationAnim', 'geneticMap']) { assert.equal(typeof C[fn], 'function', fn + ' определён'); assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null'); } }); +test('Chem8 — Phase 2 виджеты экспортированы как функции', () => { + for (const fn of ['testTube', 'indicatorScale', 'classifier', 'solubilityTable', 'activitySeries']) { + assert.equal(typeof C[fn], 'function', fn + ' реализован'); + } + assert.ok(C.testTube({ precipitate: '#88c' }).includes(' { for (const fn of ['moleTriangle', 'equationBalancer']) { assert.equal(typeof C[fn], 'function', fn + ' определён'); @@ -106,10 +113,10 @@ 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 (ch.slug === 'chemistry-8-intro') { - // intro перестроен на движок (SPA): slug задаётся через CHEM8_CFG - assert.ok(html.includes("slug:'chemistry-8-intro'"), 'intro slug в CHEM8_CFG'); - assert.ok(html.includes('/js/chem8_engine.js'), 'intro подключает движок'); + if (ch.slug === 'chemistry-8-intro' || ch.slug === 'chemistry-8-ch1') { + // перестроены на движок (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 + ' подключает движок'); } else { assert.ok(html.includes("const _TB_SLUG = '" + ch.slug + "'"), ch.file + ' slug (каркас)'); } @@ -130,6 +137,19 @@ test('Phase 1 — раздел intro перестроен на движок (SPA assert.ok(!html.includes('Раздел в разработке'), 'баннер-заглушка убран'); }); +test('Phase 2 — Глава 1 построена на движке (§10–23 + лаб/ПР + финал)', () => { + const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch1.html'), 'utf8'); + assert.ok(html.includes('id="psel-grid"'), 'para-selector'); + for (let i = 10; i <= 23; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция'); + assert.ok(html.includes('id="sec-final1"'), 'финал'); + assert.ok(html.includes('id="c-ox-cls"'), 'классификатор оксидов'); + assert.ok(html.includes('id="c-salt-sol"'), 'таблица растворимости'); + assert.ok(html.includes('Лабораторный опыт 1'), 'Лаб.1'); + assert.ok(html.includes('Практическая работа 2'), 'ПР2'); + assert.ok(html.includes('/js/chem8_ch1_widgets.js'), 'виджеты главы'); + assert.ok(!html.includes('Раздел в разработке'), 'заглушка убрана'); +}); + 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/css/chem8-textbook.css b/frontend/css/chem8-textbook.css index a34e6d0..528ceb2 100644 --- a/frontend/css/chem8-textbook.css +++ b/frontend/css/chem8-textbook.css @@ -297,6 +297,51 @@ html.dark .el-cell .s{color:var(--pri-l)} .drop-box h5{font-size:.8rem;font-weight:800;margin-bottom:8px;text-align:center;color:var(--pri-d)} html.dark .drop-box h5{color:var(--pri-l)} +/* testTube */ +.tt-svg{color:var(--pri);vertical-align:bottom} +.tt-row{display:flex;gap:18px;flex-wrap:wrap;align-items:flex-end;margin:10px 0} +.tt-cap{font-size:.84rem;color:var(--muted);text-align:center;max-width:120px} + +/* indicatorScale */ +.ind-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:10px} +.ind-row label{font-size:.85rem;font-weight:600;color:var(--muted)} +.ind-strip{height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.92rem;border:1px solid var(--border);transition:background .25s} +.ind-label{margin-top:8px;font-size:.9rem} +.ind-label b{color:var(--pri-d)}html.dark .ind-label b{color:var(--pri-l)} + +/* classifier */ +.cls-chip.on{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)} +.cls-chip.cls-ok{background:var(--ok-bg);border-color:var(--ok);color:var(--ok)} +.cls-chip.cls-bad{background:var(--fail-bg);border-color:var(--fail);color:var(--fail)} +.cls-items{display:flex;flex-wrap:wrap;gap:6px;min-height:24px} + +/* solubilityTable */ +.sol-wrap{overflow-x:auto} +.sol-tab{border-collapse:collapse;font-size:.78rem;font-family:var(--mono);min-width:520px} +.sol-tab th,.sol-tab td{border:1px solid var(--border);padding:4px 6px;text-align:center;cursor:pointer} +.sol-tab thead th{background:var(--card-soft);font-weight:800} +.sol-tab th[data-an]{background:var(--card-soft);font-weight:800} +.sol-tab td.sP{background:rgba(37,99,235,.12);color:#1d4ed8} +.sol-tab td.sM{background:rgba(245,158,11,.18);color:#b45309} +.sol-tab td.sH{background:rgba(220,38,38,.14);color:#b91c1c} +.sol-tab td.sX{background:rgba(120,120,120,.12);color:var(--muted)} +.sol-tab td.sol-dim,.sol-tab th.sol-dim{opacity:.3} +.sol-tab td.sol-hot{outline:3px solid var(--pri);outline-offset:-3px;font-weight:900} +.sol-out{margin-top:10px} + +/* activitySeries */ +.act-row{display:flex;flex-wrap:wrap;gap:4px;align-items:center} +.act-cell{font-family:var(--mono);font-weight:800;font-size:.82rem;padding:7px 9px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);cursor:pointer;transition:.12s} +.act-cell:hover{border-color:var(--pri)} +.act-cell.act-h{background:var(--card-soft);color:var(--muted);cursor:default;font-size:.74rem} +.act-cell.act-on{background:var(--pri);border-color:var(--pri);color:#fff} +.act-cell.act-disp{background:var(--ok-bg);border-color:var(--ok);color:var(--ok)} +.act-axis{display:flex;justify-content:space-between;font-size:.72rem;color:var(--muted);margin:6px 2px} +.act-out{margin-top:8px} + +/* exa-step (разбор примеров) */ +.exa-step{font-family:var(--mono);font-size:.9rem;background:var(--card-soft);border-left:3px solid var(--pri);border-radius:0 8px 8px 0;padding:8px 12px;margin:6px 0} + /* FOOTER + popup */ .foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)} .ach-popup{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(130px);background:var(--card);border:1.5px solid var(--pri);color:var(--text);padding:12px 20px;border-radius:13px;font-weight:700;box-shadow:var(--sh2);z-index:60;transition:transform .35s;display:flex;align-items:center;gap:10px;font-size:.9rem;max-width:90vw} diff --git a/frontend/js/chem8_ch1_widgets.js b/frontend/js/chem8_ch1_widgets.js new file mode 100644 index 0000000..e51ed8d --- /dev/null +++ b/frontend/js/chem8_ch1_widgets.js @@ -0,0 +1,103 @@ +/* chem8_ch1_widgets.js — виджеты Главы 1 «Важнейшие классы неорганических соединений». + * Монтируются движком: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id]. + * Используют window.Chem8: classifier, indicatorScale, solubilityTable, activitySeries. + */ +(function (W) { + 'use strict'; + function C() { return W.Chem8 || {}; } + function $(id) { return document.getElementById(id); } + function xp(n, s) { try { if (W.addXp) W.addXp(n, s); } catch (e) {} } + + /* §10 — классификатор оксидов */ + function mount_p10() { + var el = $('c-ox-cls'); if (!el || el._b || !C().classifier) return; el._b = 1; + C().classifier(el, { + items: [ + { id: 'Na2O', label: 'Na₂O', cat: 'осн' }, { id: 'CaO', label: 'CaO', cat: 'осн' }, + { id: 'CO2', label: 'CO₂', cat: 'кисл' }, { id: 'SO3', label: 'SO₃', cat: 'кисл' }, { id: 'P2O5', label: 'P₂O₅', cat: 'кисл' }, + { id: 'ZnO', label: 'ZnO', cat: 'амф' }, { id: 'Al2O3', label: 'Al₂O₃', cat: 'амф' }, + { id: 'CO', label: 'CO', cat: 'несол' }, { id: 'N2O', label: 'N₂O', cat: 'несол' } + ], + buckets: [{ cat: 'осн', label: 'Основные' }, { cat: 'кисл', label: 'Кислотные' }, { cat: 'амф', label: 'Амфотерные' }, { cat: 'несол', label: 'Несолеобразующие' }], + onCheck: function (ok) { if (ok) xp(8, 'p10-cls'); } + }); + } + + /* §13 — классификатор кислот + индикатор */ + function mount_p13() { + var cls = $('c-acid-cls'); + if (cls && !cls._b && C().classifier) { cls._b = 1; C().classifier(cls, { + items: [ + { id: 'HCl', label: 'HCl', cat: 'без' }, { id: 'H2S', label: 'H₂S', cat: 'без' }, { id: 'HBr', label: 'HBr', cat: 'без' }, + { id: 'H2SO4', label: 'H₂SO₄', cat: 'кисл' }, { id: 'HNO3', label: 'HNO₃', cat: 'кисл' }, { id: 'H3PO4', label: 'H₃PO₄', cat: 'кисл' } + ], + buckets: [{ cat: 'без', label: 'Бескислородные' }, { cat: 'кисл', label: 'Кислородсодержащие' }], + onCheck: function (ok) { if (ok) xp(8, 'p13-cls'); } + }); } + var ind = $('c-acid-ind'); if (ind && !ind._b && C().indicatorScale) { ind._b = 1; C().indicatorScale(ind, { indicator: 'лакмус', ph: 2 }); } + } + + /* §14 — ряд активности + индикатор */ + function mount_p14() { + var act = $('c-acid-act'); if (act && !act._b && C().activitySeries) { act._b = 1; C().activitySeries(act, {}); } + var ind = $('c-acid-ind2'); if (ind && !ind._b && C().indicatorScale) { ind._b = 1; C().indicatorScale(ind, { indicator: 'метилоранж', ph: 2 }); } + } + + /* §16 — классификатор оснований + индикатор (фенолфталеин) */ + function mount_p16() { + var cls = $('c-base-cls'); + if (cls && !cls._b && C().classifier) { cls._b = 1; C().classifier(cls, { + items: [ + { id: 'NaOH', label: 'NaOH', cat: 'щел' }, { id: 'KOH', label: 'KOH', cat: 'щел' }, { id: 'BaOH', label: 'Ba(OH)₂', cat: 'щел' }, + { id: 'CuOH', label: 'Cu(OH)₂', cat: 'нер' }, { id: 'FeOH', label: 'Fe(OH)₃', cat: 'нер' }, { id: 'MgOH', label: 'Mg(OH)₂', cat: 'нер' } + ], + buckets: [{ cat: 'щел', label: 'Щёлочи (растворимые)' }, { cat: 'нер', label: 'Нерастворимые' }], + onCheck: function (ok) { if (ok) xp(8, 'p16-cls'); } + }); } + var ind = $('c-base-ind'); if (ind && !ind._b && C().indicatorScale) { ind._b = 1; C().indicatorScale(ind, { indicator: 'фенолфталеин', ph: 12 }); } + } + + /* §17 — индикатор нейтрализации */ + function mount_p17() { var ind = $('c-neutral-ind'); if (ind && !ind._b && C().indicatorScale) { ind._b = 1; C().indicatorScale(ind, { indicator: 'фенолфталеин', ph: 12 }); } } + + /* §18 — индикатор (ПР2 нейтрализация) */ + function mount_p18() { var ind = $('c-pr2-ind'); if (ind && !ind._b && C().indicatorScale) { ind._b = 1; C().indicatorScale(ind, { indicator: 'лакмус', ph: 7 }); } } + + /* §19 — таблица растворимости */ + function mount_p19() { var s = $('c-salt-sol'); if (s && !s._b && C().solubilityTable) { s._b = 1; C().solubilityTable(s, {}); } } + + /* §20 — растворимость + ряд активности (соль + металл) */ + function mount_p20() { + var s = $('c-salt-sol2'); if (s && !s._b && C().solubilityTable) { s._b = 1; C().solubilityTable(s, {}); } + var a = $('c-salt-act'); if (a && !a._b && C().activitySeries) { a._b = 1; C().activitySeries(a, {}); } + } + + /* §23 — пошаговый решатель расчётных задач по классам */ + var ST = [ + { eq: 'CaO + 2HCl → CaCl₂ + H₂O', given: 'Дано: m(CaO) = 28 г. Найти m(CaCl₂). M(CaO)=56, M(CaCl₂)=111.', + steps: ['n(CaO) = m/M = 28/56 = 0,5 моль.', 'n(CaO):n(CaCl₂) = 1:1 → n(CaCl₂)=0,5 моль.', 'm(CaCl₂) = n·M = 0,5·111 = 55,5 г. Ответ: 55,5 г.'] }, + { eq: 'Zn + H₂SO₄ → ZnSO₄ + H₂↑', given: 'Дано: n(Zn) = 2 моль. Найти V(H₂) при н.у.', + steps: ['n(Zn):n(H₂) = 1:1 → n(H₂)=2 моль.', 'V(H₂) = n·Vm = 2·22,4 = 44,8 л. Ответ: 44,8 л.'] }, + { eq: '2NaOH + H₂SO₄ → Na₂SO₄ + 2H₂O', given: 'Дано: n(H₂SO₄) = 0,5 моль. Найти n(NaOH).', + steps: ['n(NaOH):n(H₂SO₄) = 2:1 → n(NaOH)=2·0,5=1 моль.', 'Ответ: 1 моль NaOH.'] } + ]; + function mount_p23() { + var pick = $('c-calc-pick'), out = $('c-calc-out'), bStep = $('c-calc-step'), bAll = $('c-calc-all'); if (!pick || pick._b) return; pick._b = 1; + ST.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 = ST[cur]; + var html = '' + p.eq + '
' + p.given + '
'; + 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 < ST[cur].steps.length) { shown++; render(); } }); + bAll.addEventListener('click', function () { shown = ST[cur].steps.length; render(); }); + render(); + } + + W.CHEM8_WIDGETS = { p13: mount_p13, p16: mount_p16, p17: mount_p17, p18: mount_p18 }; + W.FLAG_MOUNTS = { p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p23: mount_p23 }; +})(window); diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js index 9a411c3..dee0aa4 100644 --- a/frontend/js/chem8_svg.js +++ b/frontend/js/chem8_svg.js @@ -350,6 +350,211 @@ }); } + /* ────────────────────────────────────────────────────────────────────────── + testTube(opts) -> SVG-строка пробирки. opts: {fill, color, precipitate, gas, + label}. fill/color — цвет раствора; precipitate — цвет осадка на дне; + gas:true — пузырьки; label — подпись под пробиркой. + ────────────────────────────────────────────────────────────────────────── */ + function testTube(opts) { + opts = opts || {}; + var liq = opts.color || opts.fill || '#dbeafe'; + var prec = opts.precipitate || null; + var gas = !!opts.gas; + var bubbles = ''; + if (gas) for (var i = 0; i < 5; i++) { + var cx = 26 + (i % 3) * 7, cy = 60 - i * 8; + bubbles += ''; + } + var precSvg = prec ? '' : ''; + return '' + + '' + + '' + + precSvg + + '' + bubbles + '' + + '' + + '' + + (opts.label ? '' + escapeHtml(opts.label) + '' : '') + + ''; + } + + /* ────────────────────────────────────────────────────────────────────────── + indicatorScale(mount, opts) — индикатор + шкала pH. Слайдер pH 0–14, + выбор индикатора (лакмус/фенолфталеин/метилоранж), окраска полоски. + ────────────────────────────────────────────────────────────────────────── */ + var INDICATORS = { + 'лакмус': function (ph) { return ph < 5 ? ['#dc2626', 'красный (кислота)'] : ph > 8 ? ['#2563eb', 'синий (щёлочь)'] : ['#7c3aed', 'фиолетовый (нейтр.)']; }, + 'фенолфталеин': function (ph) { return ph >= 8.2 ? ['#db2777', 'малиновый (щёлочь)'] : ['#f8fafc', 'бесцветный']; }, + 'метилоранж': function (ph) { return ph < 3.1 ? ['#dc2626', 'красный (кислота)'] : ph > 4.4 ? ['#f59e0b', 'жёлтый'] : ['#fb923c', 'оранжевый']; } + }; + function indicatorScale(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + opts = opts || {}; + var inds = Object.keys(INDICATORS); + host.innerHTML = + '
' + + '
'; + var sel = host.querySelector('.ind-sel'), ph = host.querySelector('.ind-ph'), + phv = host.querySelector('.ind-phv'), strip = host.querySelector('.ind-strip'), lab = host.querySelector('.ind-label'); + function upd() { + var v = parseFloat(ph.value), pair = INDICATORS[sel.value](v); + phv.textContent = 'pH ' + String(v).replace('.', ','); + strip.style.background = pair[0]; + strip.style.color = (pair[0] === '#f8fafc' || pair[0] === '#f59e0b') ? '#1c1917' : '#fff'; + strip.textContent = pair[1]; + lab.innerHTML = 'Среда: ' + (v < 7 ? 'кислая' : v > 7 ? 'щелочная' : 'нейтральная') + ' · ' + sel.value + ' → ' + pair[1]; + } + sel.addEventListener('change', upd); ph.addEventListener('input', upd); upd(); + return { el: host }; + } + + /* ────────────────────────────────────────────────────────────────────────── + classifier(mount, {items, buckets, onCheck}) — клик-классификатор (DnD без drag). + items: [{id,label,cat}]; buckets: [{cat,label}]. Клик по чипу → выбран; клик + по корзине → положить. «Проверить» подсвечивает верно/неверно. +XP по onCheck. + ────────────────────────────────────────────────────────────────────────── */ + function classifier(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + opts = opts || {}; var items = opts.items || [], buckets = opts.buckets || []; + var placed = {}; // id -> cat + var sel = null; + host.innerHTML = + '
' + items.map(function (it) { + return ''; + }).join('') + '
' + + '
' + buckets.map(function (b) { + return '
' + b.label + '
'; + }).join('') + '
' + + '
' + + ''; + var out = host.querySelector('.cls-out'); + function findItem(id) { return items.filter(function (x) { return x.id === id; })[0]; } + function selectChip(chip) { + if (sel) sel.classList.remove('on'); sel = chip; chip.classList.add('on'); + } + host.querySelectorAll('.cls-chip').forEach(function (chip) { + chip.addEventListener('click', function () { selectChip(chip); }); + }); + host.querySelectorAll('.cls-zone').forEach(function (zone) { + zone.addEventListener('click', function () { + if (!sel) return; + var id = sel.getAttribute('data-id'); + placed[id] = zone.getAttribute('data-cat'); + zone.querySelector('.cls-items').appendChild(sel); + sel.classList.remove('on'); sel.classList.add('placed'); sel = null; + }); + }); + host.querySelector('.cls-check').addEventListener('click', function () { + var ok = 0, total = items.length; + items.forEach(function (it) { + var chip = host.querySelector('.cls-chip[data-id="' + it.id + '"]'); + var correct = placed[it.id] === it.cat; + chip.classList.remove('cls-ok', 'cls-bad'); + chip.classList.add(correct ? 'cls-ok' : 'cls-bad'); + if (correct) ok++; + }); + out.style.display = 'block'; + out.className = 'out cls-out ' + (ok === total ? 'ok' : 'bad'); + out.textContent = 'Верно: ' + ok + ' из ' + total + (ok === total ? '. Отлично!' : '. Исправь выделенные.'); + if (typeof opts.onCheck === 'function') opts.onCheck(ok === total, ok, total); + }); + host.querySelector('.cls-reset').addEventListener('click', function () { + placed = {}; sel = null; + var pool = host.querySelector('.cls-pool'); + host.querySelectorAll('.cls-chip').forEach(function (c) { c.classList.remove('placed', 'on', 'cls-ok', 'cls-bad'); pool.appendChild(c); }); + out.style.display = 'none'; + }); + return { el: host, result: function () { return placed; } }; + } + + /* ────────────────────────────────────────────────────────────────────────── + solubilityTable(mount, opts) — таблица растворимости (катион×анион). + Клик по катиону и аниону → подсветка ячейки + вердикт (Р/М/Н/—). + ────────────────────────────────────────────────────────────────────────── */ + var SOL_ANIONS = ['OH', 'Cl', 'NO3', 'SO4', 'CO3', 'PO4', 'S']; + var SOL_CATIONS = ['Na', 'K', 'NH4', 'Ba', 'Ca', 'Mg', 'Al', 'Zn', 'Fe2', 'Fe3', 'Cu', 'Ag', 'Pb']; + // P раств., M малораств., H нераств., '-' не существует/разлагается + var SOL = { + OH: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'M',Mg:'H',Al:'H',Zn:'H',Fe2:'H',Fe3:'H',Cu:'H',Ag:'-',Pb:'H'}, + Cl: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'H',Pb:'M'}, + NO3: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'P',Pb:'P'}, + SO4: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'M',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'M',Pb:'H'}, + CO3: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'H',Mg:'H',Al:'-',Zn:'H',Fe2:'H',Fe3:'-',Cu:'H',Ag:'H',Pb:'H'}, + PO4: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'H',Mg:'H',Al:'H',Zn:'H',Fe2:'H',Fe3:'H',Cu:'H',Ag:'H',Pb:'H'}, + S: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'-',Zn:'H',Fe2:'H',Fe3:'-',Cu:'H',Ag:'H',Pb:'H'} + }; + var SOL_LABEL = { P: ['Р', 'растворимо'], M: ['М', 'малорастворимо'], H: ['Н', 'нерастворимо'], '-': ['—', 'не существует / разлагается'] }; + var CAT_HTML = { Na:'Na⁺', K:'K⁺', NH4:'NH₄⁺', Ba:'Ba²⁺', Ca:'Ca²⁺', Mg:'Mg²⁺', Al:'Al³⁺', Zn:'Zn²⁺', Fe2:'Fe²⁺', Fe3:'Fe³⁺', Cu:'Cu²⁺', Ag:'Ag⁺', Pb:'Pb²⁺' }; + var AN_HTML = { OH:'OH⁻', Cl:'Cl⁻', NO3:'NO₃⁻', SO4:'SO₄²⁻', CO3:'CO₃²⁻', PO4:'PO₄³⁻', S:'S²⁻' }; + function solubilityTable(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + opts = opts || {}; + var th = 'ион' + SOL_CATIONS.map(function (c) { return '' + CAT_HTML[c] + ''; }).join('') + ''; + var rows = SOL_ANIONS.map(function (an) { + return '' + AN_HTML[an] + '' + SOL_CATIONS.map(function (c) { + var v = SOL[an][c]; var cls = v === 'P' ? 'sP' : v === 'M' ? 'sM' : v === 'H' ? 'sH' : 'sX'; + return '' + SOL_LABEL[v][0] + ''; + }).join('') + ''; + }).join(''); + host.innerHTML = '
' + th + '' + rows + '
' + + '
Кликни по катиону и аниону — узнаешь растворимость соли/основания.
'; + var out = host.querySelector('.sol-out'), selCat = null, selAn = null; + function upd() { + host.querySelectorAll('.sol-tab td').forEach(function (td) { + var on = (!selCat || td.getAttribute('data-cat') === selCat) && (!selAn || td.getAttribute('data-an') === selAn); + td.classList.toggle('sol-dim', (selCat || selAn) && !on); + td.classList.toggle('sol-hot', selCat && selAn && td.getAttribute('data-cat') === selCat && td.getAttribute('data-an') === selAn); + }); + if (selCat && selAn) { + var v = SOL[selAn][selCat]; + out.className = 'out sol-out ' + (v === 'H' ? 'ok' : ''); + out.innerHTML = CAT_HTML[selCat] + ' + ' + AN_HTML[selAn] + ' → ' + SOL_LABEL[v][1] + '' + + (v === 'H' ? ' (выпадает осадок ↓ — реакция идёт)' : v === 'P' ? ' (осадок не образуется)' : ''); + } + } + host.querySelectorAll('[data-cat]').forEach(function (el) { + if (el.tagName === 'TH') el.addEventListener('click', function () { selCat = el.getAttribute('data-cat'); upd(); }); + }); + host.querySelectorAll('th[data-an]').forEach(function (el) { el.addEventListener('click', function () { selAn = el.getAttribute('data-an'); upd(); }); }); + host.querySelectorAll('.sol-tab td').forEach(function (td) { + td.addEventListener('click', function () { selCat = td.getAttribute('data-cat'); selAn = td.getAttribute('data-an'); upd(); }); + }); + return { el: host }; + } + + /* ────────────────────────────────────────────────────────────────────────── + activitySeries(mount, opts) — ряд активности металлов. Клик по металлу → + подсветка; показывает, какие металлы он вытесняет и реакцию с кислотой. + ────────────────────────────────────────────────────────────────────────── */ + var ACT = ['K', 'Ca', 'Na', 'Mg', 'Al', 'Zn', 'Fe', 'Ni', 'Sn', 'Pb', 'H', 'Cu', 'Hg', 'Ag', 'Pt', 'Au']; + function activitySeries(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host) return null; + host.innerHTML = '
' + ACT.map(function (m) { + return ''; + }).join('') + '
← восстановит. свойства растутактивность падает →
' + + '
Кликни по металлу — узнаешь его активность и реакцию с кислотами.
'; + var out = host.querySelector('.act-out'); + host.querySelectorAll('.act-cell').forEach(function (c) { + c.addEventListener('click', function () { + var m = c.getAttribute('data-m'); if (m === 'H') return; + var idx = ACT.indexOf(m), hIdx = ACT.indexOf('H'); + host.querySelectorAll('.act-cell').forEach(function (x) { x.classList.remove('act-on', 'act-disp'); }); + c.classList.add('act-on'); + ACT.forEach(function (mm, i) { if (i > idx && mm !== 'H') host.querySelector('.act-cell[data-m="' + mm + '"]').classList.add('act-disp'); }); + var withAcid = idx < hIdx ? 'вытесняет водород $\\text{H}_2$ из растворов кислот' : 'НЕ вытесняет водород из кислот (стоит после H)'; + out.className = 'out act-out'; + out.innerHTML = '' + m + ': ' + withAcid + '. Вытесняет из растворов солей все металлы, стоящие правее (подсвечены).'; + if (global.window && global.window.chem8RenderMath) try { global.window.chem8RenderMath(out); } catch (e) {} + }); + }); + return { el: host }; + } + /* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */ function notImplemented(name) { return function () { @@ -374,17 +579,18 @@ fmt: fmt, moleTriangle: moleTriangle, // §6 — треугольник n–m–M equationBalancer: equationBalancer, // §8 — балансировщик уравнений - // заглушки (см. план, разд. B) — наполняются в Phase 2–6 - testTube: notImplemented('testTube'), // §18,25 — пробирка: осадок/газ/окраска + // готово (Phase 2 — классы неорганических соединений) + testTube: testTube, // §18,25 — пробирка: осадок/газ/окраска + indicatorScale: indicatorScale, // §13,14,16,17 — индикатор + шкала pH + classifier: classifier, // §10,13,16,19 — клик-классификатор + solubilityTable: solubilityTable, // §19,20 — таблица растворимости + activitySeries: activitySeries, // §14,20 — ряд активности металлов + // заглушки (см. план, разд. B) — наполняются в Phase 3–6 oxStateCalc: notImplemented('oxStateCalc'), // §42 — калькулятор степени окисления redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма - solubilityTable: notImplemented('solubilityTable'), // §19,20,48 — таблица растворимости - activitySeries: notImplemented('activitySeries'), // §14,20 — ряд активности металлов - miniPeriodic: notImplemented('miniPeriodic'), // §1,26,34 — мини-ПСХЭ с подсветкой - indicatorScale: notImplemented('indicatorScale'), // §13,14,16,17 — индикатор + шкала pH + miniPeriodic: notImplemented('miniPeriodic'), // §26,34 — мини-ПСХЭ с подсветкой dissociationAnim: notImplemented('dissociationAnim'),// §47 — анимация растворения - classifier: notImplemented('classifier'), // §10,13,16,19,46 — DnD-классификатор geneticMap: notImplemented('geneticMap') // §22 — генетическая карта-граф классов }; diff --git a/frontend/textbooks/chemistry_8_ch1.html b/frontend/textbooks/chemistry_8_ch1.html index 3ab5580..3f2e2bf 100644 --- a/frontend/textbooks/chemistry_8_ch1.html +++ b/frontend/textbooks/chemistry_8_ch1.html @@ -7,141 +7,344 @@ Химия 8 · Глава 1 · «Важнейшие классы неорганических соединений» - + + + - + +
-
- - - К разделам - +
-
Глава 1 · § 10–23
-

Важнейшие классы неорганических соединений

+

Химия 8 · Глава 1

+
Оксиды, кислоты, основания и соли: состав, классификация, свойства, получение и генетическая связь
- + К разделам +
-
-
-
- -
-
-

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

-

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

-
-
+
+
+
+

Четыре класса, из которых построена неорганическая химия

+

Оксиды, кислоты, основания и соли связаны между собой превращениями. Научившись узнавать класс вещества по формуле и предсказывать его реакции, ты сможешь «читать» химию как язык.

+
+ +
Прогресс главы
0%
+
+
+
-
- - Содержание раздела +
Параграфы главы
+ +
§ 10

Оксиды. Состав и классификация

+
§ 11

Химические свойства оксидов

+
§ 12

Получение и применение оксидов

+
§ 13

Кислоты. Состав и классификация

+
§ 14

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

+
§ 15

Получение и применение кислот

+
§ 16

Основания

+
§ 17

Химические свойства оснований

+
§ 18

Получение оснований · Лаб. 1 · ПР 2

+
§ 19

Соли. Состав и классификация

+
§ 20

Химические свойства солей · Лаб. 2

+
§ 21

Получение и применение солей

+
§ 22

Взаимосвязь классов · ПР 3

+
§ 23

Решение расчётных задач

+

Финал главы

-
    -
  • § 10Оксиды. Состав и классификация оксидов
  • -
  • § 11Химические свойства оксидов
  • -
  • § 12Получение и применение оксидов
  • -
  • § 13Кислоты. Состав и классификация кислот
  • -
  • § 14Химические свойства кислот
  • -
  • § 15Получение и применение кислот
  • -
  • § 16Основания
  • -
  • § 17Химические свойства оснований
  • -
  • § 18Получение и применение оснований
  • -
  • Лабораторный опыт 1. Получение нерастворимого основания
  • -
  • Практическая работа 2. Изучение реакции нейтрализации
  • -
  • § 19Соли. Состав и классификация солей
  • -
  • § 20Химические свойства солей
  • -
  • Лабораторный опыт 2. Взаимодействие растворов солей с металлами
  • -
  • § 21Получение и применение солей
  • -
  • § 22Взаимосвязь между классами основных неорганических веществ
  • -
  • Практическая работа 3. Решение экспериментальных задач
  • -
  • § 23Решение расчётных задач по теме «Основные классы неорганических соединений»
  • -
+
-
- Интерактивный учебник «Химия — 8 класс» · Глава 1 · LearnSpace -
+
Интерактивный учебник «Химия — 8 класс» · Глава 1 · «Важнейшие классы неорганических соединений» · LearnSpace
+
Достижение!