diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js index 2d7ab7f..39595d3 100644 --- a/backend/tests/chemistry8-page.test.js +++ b/backend/tests/chemistry8-page.test.js @@ -89,6 +89,12 @@ test('ch1: генетическая карта §22 монтируется (U3)' assert.ok(doc.querySelectorAll('#c-genetic .gm-edge').length >= 6, 'граф классов §22'); }); +test('ch1: карта связей в финале главы монтируется (U6)', async () => { + const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js'); + doc.defaultView.goTo('final1'); await wait(120); + assert.ok(doc.querySelectorAll('#c-concept .gm-edge').length >= 3, 'карта связей понятий финала'); +}); + /* ── Глава 2 ── */ test('ch2: SPA без ошибок, 6 карточек, §24 активен, ПСХЭ', async () => { const { doc, errors } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js'); diff --git a/frontend/js/chem8_ch1_widgets.js b/frontend/js/chem8_ch1_widgets.js index f2e377c..fba84d3 100644 --- a/frontend/js/chem8_ch1_widgets.js +++ b/frontend/js/chem8_ch1_widgets.js @@ -100,7 +100,8 @@ /* §22 — генетическая карта классов */ function mount_p22() { var el = $('c-genetic'); if (el && !el._b && C().geneticMap) { el._b = 1; C().geneticMap(el, {}); } } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"ox","t":"Оксид","x":20,"y":22},{"id":"acid","t":"Кислота","x":160,"y":22,"c":"#2563eb"},{"id":"base","t":"Основание","x":20,"y":95,"c":"#0d9488"},{"id":"salt","t":"Соль","x":330,"y":55,"c":"#d97706"}],"edges":[{"f":"ox","t":"acid","label":"кислотный оксид + вода → кислота"},{"f":"acid","t":"base","label":"нейтрализация → соль + вода"},{"f":"acid","t":"salt","label":"кислота + металл/оксид → соль"},{"f":"base","t":"salt","label":"основание + кислота → соль"}]}); } } 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, p22: mount_p22, p23: mount_p23 }; + W.FLAG_MOUNTS = { final1: mount_final1, p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p22: mount_p22, p23: mount_p23 }; })(window); diff --git a/frontend/js/chem8_ch2_widgets.js b/frontend/js/chem8_ch2_widgets.js index 2e9ac8b..a44522c 100644 --- a/frontend/js/chem8_ch2_widgets.js +++ b/frontend/js/chem8_ch2_widgets.js @@ -69,7 +69,8 @@ el.querySelector('.amph-reset').addEventListener('click', reset); reset(); } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"per","t":"Период","x":20,"y":22},{"id":"grp","t":"Группа","x":20,"y":95},{"id":"fam","t":"Семейство","x":160,"y":55},{"id":"prop","t":"Свойства","x":330,"y":55}],"edges":[{"f":"per","t":"prop","label":"номер периода = число слоёв"},{"f":"grp","t":"prop","label":"номер группы = внешние e⁻"},{"f":"fam","t":"grp","label":"одна группа — одно семейство"}]}); } } W.CHEM8_WIDGETS = { p25: mount_p25 }; - W.FLAG_MOUNTS = { p24: mount_p24, p26: mount_p26, p28: mount_p28 }; + W.FLAG_MOUNTS = { final1: mount_final1, p24: mount_p24, p26: mount_p26, p28: mount_p28 }; })(window); diff --git a/frontend/js/chem8_ch3_widgets.js b/frontend/js/chem8_ch3_widgets.js index 08e7599..7f5bdf9 100644 --- a/frontend/js/chem8_ch3_widgets.js +++ b/frontend/js/chem8_ch3_widgets.js @@ -91,7 +91,8 @@ if (W.chem8RenderMath) try { W.chem8RenderMath(panel); } catch (e) {} } }); } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"nuc","t":"Ядро","x":20,"y":55},{"id":"prot","t":"Протоны","x":170,"y":22},{"id":"neut","t":"Нейтроны","x":170,"y":95},{"id":"elec","t":"Электроны","x":330,"y":55}],"edges":[{"f":"nuc","t":"prot","label":"Z = число протонов"},{"f":"nuc","t":"neut","label":"N нейтронов (A = Z + N)"},{"f":"prot","t":"elec","label":"Z = 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 }; + W.FLAG_MOUNTS = { final1: mount_final1, p34: mount_p34, p35: mount_p35 }; })(window); diff --git a/frontend/js/chem8_ch4_widgets.js b/frontend/js/chem8_ch4_widgets.js index 013efc4..4369bac 100644 --- a/frontend/js/chem8_ch4_widgets.js +++ b/frontend/js/chem8_ch4_widgets.js @@ -13,7 +13,8 @@ var mol = $('c-mol'); if (mol && !mol._b && M()) { mol._b = 1; M().molModel(mol, 'H2O'); } } function mount_p41() { var el = $('c-lattice'); if (el && !el._b && M()) { el._b = 1; M().latticeViewer(el, 'ionic'); } } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"cov","t":"Ковалент.","x":20,"y":22},{"id":"ion","t":"Ионная","x":20,"y":95},{"id":"met","t":"Металлич.","x":160,"y":55},{"id":"lat","t":"Решётка","x":330,"y":22},{"id":"prop","t":"Свойства","x":330,"y":95}],"edges":[{"f":"cov","t":"lat","label":"ковалентная → атомная/молек. решётка"},{"f":"ion","t":"lat","label":"ионная → ионная решётка"},{"f":"met","t":"lat","label":"металлическая → металл. решётка"},{"f":"lat","t":"prop","label":"тип решётки определяет свойства"}]}); } } W.CHEM8_WIDGETS = {}; - W.FLAG_MOUNTS = { p37: mount_p37, p38: mount_p38, p41: mount_p41 }; + W.FLAG_MOUNTS = { final1: mount_final1, p37: mount_p37, p38: mount_p38, p41: mount_p41 }; })(window); diff --git a/frontend/js/chem8_ch5_widgets.js b/frontend/js/chem8_ch5_widgets.js index 5639825..193b77d 100644 --- a/frontend/js/chem8_ch5_widgets.js +++ b/frontend/js/chem8_ch5_widgets.js @@ -52,7 +52,8 @@ bAll.addEventListener('click', function () { shown = R[cur].steps.length; render(); }); render(); } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"so","t":"Степ. ок.","x":20,"y":55},{"id":"oxi","t":"Окисление","x":170,"y":22},{"id":"red","t":"Восстан.","x":170,"y":95},{"id":"bal","t":"Баланс","x":330,"y":55,"c":"#d97706"}],"edges":[{"f":"so","t":"oxi","label":"с.о. растёт (отдача e⁻)"},{"f":"so","t":"red","label":"с.о. падает (приём e⁻)"},{"f":"oxi","t":"bal","label":"отдано e⁻"},{"f":"red","t":"bal","label":"= принято e⁻"}]}); } } W.CHEM8_WIDGETS = { p42: mount_p42 }; - W.FLAG_MOUNTS = { p44: mount_p44 }; + W.FLAG_MOUNTS = { final1: mount_final1, p44: mount_p44 }; })(window); diff --git a/frontend/js/chem8_ch6_widgets.js b/frontend/js/chem8_ch6_widgets.js index e1d8106..1dd4b87 100644 --- a/frontend/js/chem8_ch6_widgets.js +++ b/frontend/js/chem8_ch6_widgets.js @@ -78,7 +78,8 @@ /* §47 — анимация растворения/диссоциации */ function mount_p47() { var el = $('c-dissoc'); if (el && !el._b && C().dissociationAnim) { el._b = 1; C().dissociationAnim(el, { substance: 'NaCl' }); } } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"mix","t":"Смесь","x":20,"y":55},{"id":"sol","t":"Раствор","x":170,"y":55,"c":"#0891b2"},{"id":"sb","t":"Растворим.","x":330,"y":22},{"id":"w","t":"w (доля)","x":330,"y":55},{"id":"c","t":"c (моль/л)","x":330,"y":95}],"edges":[{"f":"mix","t":"sol","label":"однородная смесь = раствор"},{"f":"sol","t":"sb","label":"растворимость: г / 100 г воды"},{"f":"sol","t":"w","label":"массовая доля w = m / m"},{"f":"sol","t":"c","label":"молярная концентрация c = n/V"}]}); } } W.CHEM8_WIDGETS = { p46: mount_p46, p50: mount_p50, p51: mount_p51 }; - W.FLAG_MOUNTS = { p47: mount_p47, p48: mount_p48 }; + W.FLAG_MOUNTS = { final1: mount_final1, p47: mount_p47, p48: mount_p48 }; })(window); diff --git a/frontend/js/chem8_intro_widgets.js b/frontend/js/chem8_intro_widgets.js index 04b61f4..d1a2d7f 100644 --- a/frontend/js/chem8_intro_widgets.js +++ b/frontend/js/chem8_intro_widgets.js @@ -139,7 +139,8 @@ bAll.addEventListener('click', function () { shown = ST[cur].steps.length; render(); }); render(); } + function mount_final1(){ var el=$('c-concept'); if(el&&!el._b&&C().conceptMap){ el._b=1; C().conceptMap(el,{"nodes":[{"id":"n","t":"n, моль","x":170,"y":55,"c":"#d97706"},{"id":"m","t":"m, г","x":20,"y":22},{"id":"M","t":"M, г/моль","x":20,"y":95},{"id":"V","t":"V, л","x":330,"y":22},{"id":"N","t":"N частиц","x":330,"y":95}],"edges":[{"f":"m","t":"n","label":"n = m / M"},{"f":"M","t":"n","label":"M = m / n"},{"f":"n","t":"V","label":"V = n · 22,4 (газ, н.у.)"},{"f":"n","t":"N","label":"N = n · 6,02·10²³"}]}); } } W.CHEM8_WIDGETS = { p1: mount_p1, p2: mount_p2, p3: mount_p3, p4: mount_p4, p5: mount_p5, pr1: mount_pr1 }; - W.FLAG_MOUNTS = { p6: mount_p6, p7: mount_p7, p8: mount_p8, p9: mount_p9 }; + W.FLAG_MOUNTS = { final1: mount_final1, p6: mount_p6, p7: mount_p7, p8: mount_p8, p9: mount_p9 }; })(window); diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js index a293bc8..4b61ebb 100644 --- a/frontend/js/chem8_svg.js +++ b/frontend/js/chem8_svg.js @@ -850,6 +850,41 @@ return { el: host }; } + /* ────────────────────────────────────────────────────────────────────────── + conceptMap(mount, {nodes, edges}) — обобщённая карта связей понятий главы. + nodes: [{id, t, x, y, c?}]; edges: [{f, t, label}]. Клик по ребру → подпись. + Используется в финалах глав (U6). + ────────────────────────────────────────────────────────────────────────── */ + function conceptMap(mount, opts) { + var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount; + if (!host || !opts) return null; + var nodes = opts.nodes || [], edges = opts.edges || []; + var byId = {}; nodes.forEach(function (n) { byId[n.id] = n; }); + var W0 = opts.w || 430, H0 = opts.h || 150; + function cx(n) { return n.x + 44; } function cy(n) { return n.y + 16; } + var edgesSvg = edges.map(function (e, i) { + var a = byId[e.f], b = byId[e.t]; if (!a || !b) return ''; + return ''; + }).join(''); + var nodesSvg = nodes.map(function (n) { + var c = n.c || 'var(--pri,#d97706)'; + return '' + + '' + n.t + ''; + }).join(''); + host.innerHTML = '
' + edgesSvg + nodesSvg + '
' + + '
Кликни по связи — увидишь, как понятия главы связаны.
'; + var out = host.querySelector('.gm-out'); + host.querySelectorAll('.gm-edge').forEach(function (ln) { + ln.style.cursor = 'pointer'; + ln.addEventListener('click', function () { + host.querySelectorAll('.gm-edge').forEach(function (x) { x.setAttribute('stroke', 'var(--muted,#888)'); x.setAttribute('stroke-width', '2.5'); }); + ln.setAttribute('stroke', 'var(--pri,#d97706)'); ln.setAttribute('stroke-width', '4'); + out.className = 'out gm-out ok'; out.innerHTML = '' + edges[+ln.getAttribute('data-i')].label + ''; + }); + }); + return { el: host }; + } + /* ────────────────────────────────────────────────────────────────────────── dissociationAnim(mount, {substance}) — анимация растворения/диссоциации: вещество распадается на ионы, окружённые молекулами воды. §47. @@ -932,8 +967,9 @@ // готово (Phase 6 — ОВР) oxStateCalc: oxStateCalc, // §42 — калькулятор степени окисления oxStates: oxStates, // степени окисления (чистая функция) - // готово (Phase 8/U3 — апгрейд) + // готово (Phase 8/U3,U6 — апгрейд) geneticMap: geneticMap, // §22 — генетическая карта-граф классов + conceptMap: conceptMap, // финалы глав — карта связей понятий (U6) dissociationAnim: dissociationAnim, // §47 — анимация растворения/диссоциации // редокс-баланс §44 реализован пошагово в chem8_ch5_widgets (преднабор) redoxBalancer: notImplemented('redoxBalancer'), diff --git a/frontend/textbooks/chemistry_8_ch1.html b/frontend/textbooks/chemistry_8_ch1.html index 77f9b44..bca8d37 100644 --- a/frontend/textbooks/chemistry_8_ch1.html +++ b/frontend/textbooks/chemistry_8_ch1.html @@ -346,7 +346,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 1','Босс: классы неорганических соединений','оксиды · кислоты · основания · соли','Шесть интегрированных задач на всю главу. Победи босса — получи ачивку «Классы веществ покорены».') +makeCard('rule','Шпаргалка главы',null,'

Оксиды

осн · кисл · амф

Кислоты

HₓAc, основность

Основания

Me(OH)ₙ

Соли

катион + анион
') +'

Реши все задачи ниже — за каждую +5 XP, за полную победу — ачивка и бонус.

' - +secNav('p23',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p23',null); } diff --git a/frontend/textbooks/chemistry_8_ch2.html b/frontend/textbooks/chemistry_8_ch2.html index 825c987..d381c49 100644 --- a/frontend/textbooks/chemistry_8_ch2.html +++ b/frontend/textbooks/chemistry_8_ch2.html @@ -189,7 +189,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 2','Босс: периодический закон','семейства · период · группа','Пять интегрированных задач по всей главе. Победи босса — ачивка «Периодический закон освоен».') +makeCard('rule','Шпаргалка главы',null,'

Период

= число слоёв

Группа

= внешние e⁻

Семейства

щелочные · галогены

Закон

периодичность по Z
') +'

Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.

' - +secNav('p28',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p28',null); } diff --git a/frontend/textbooks/chemistry_8_ch3.html b/frontend/textbooks/chemistry_8_ch3.html index 4cd5580..4d3dee4 100644 --- a/frontend/textbooks/chemistry_8_ch3.html +++ b/frontend/textbooks/chemistry_8_ch3.html @@ -224,7 +224,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 3','Босс: строение атома','Z · A=Z+N · слои · периодичность','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Строение атома освоено».') +makeCard('rule','Шпаргалка главы',null,'

Состав

ядро + e⁻

Масса

A = Z + N

Слои

2n² электронов

Тренд

период → неметалл↑
') +'

Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.

' - +secNav('p35',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p35',null); } diff --git a/frontend/textbooks/chemistry_8_ch4.html b/frontend/textbooks/chemistry_8_ch4.html index c0c60f4..b44d291 100644 --- a/frontend/textbooks/chemistry_8_ch4.html +++ b/frontend/textbooks/chemistry_8_ch4.html @@ -218,7 +218,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 4','Босс: химическая связь','ковалентная · ионная · металлическая','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Химическая связь освоена».') +makeCard('rule','Шпаргалка главы',null,'

Ковалентная

общие пары

Ионная

передача e⁻

Металлическая

электронный газ

Решётка

тип → свойства
') +'

Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.

' - +secNav('p41',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p41',null); } diff --git a/frontend/textbooks/chemistry_8_ch5.html b/frontend/textbooks/chemistry_8_ch5.html index fa3c743..25b85ac 100644 --- a/frontend/textbooks/chemistry_8_ch5.html +++ b/frontend/textbooks/chemistry_8_ch5.html @@ -178,7 +178,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 5','Босс: ОВР','с.о. · окислитель/восстановитель · баланс','Шесть интегрированных задач по всей главе. Победи босса — ачивка «ОВР освоены».') +makeCard('rule','Шпаргалка главы',null,'

С.о.

H +1, O −2, Σ=0

Окисление

−e⁻, с.о. ↑

Восстановление

+e⁻, с.о. ↓

Баланс

отдано = принято
') +'

Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.

' - +secNav('p45',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p45',null); } diff --git a/frontend/textbooks/chemistry_8_ch6.html b/frontend/textbooks/chemistry_8_ch6.html index 3e7b1f6..44bf94d 100644 --- a/frontend/textbooks/chemistry_8_ch6.html +++ b/frontend/textbooks/chemistry_8_ch6.html @@ -223,7 +223,7 @@ function bfinal(){ document.getElementById('final1-body').innerHTML = hero('final','Финал главы 6','Босс: растворы','смеси · растворимость · w · c','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Растворы освоены».') +makeCard('rule','Шпаргалка главы',null,'

Массовая доля

w = m(в-ва)/m(р-ра)

Концентрация

c = n/V

Растворимость

г / 100 г воды

Смеси

однород./неоднород.
') +'

Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.

' - +secNav('p52',null); } + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p52',null); } diff --git a/frontend/textbooks/chemistry_8_intro.html b/frontend/textbooks/chemistry_8_intro.html index ecf4cde..96cfe93 100644 --- a/frontend/textbooks/chemistry_8_intro.html +++ b/frontend/textbooks/chemistry_8_intro.html @@ -381,7 +381,7 @@ function build_final1(){ +'

Число частиц

N = n · 6,02·10²³
' +'

Связка

m → n → V, N
') +'

Реши задачи ниже — за каждую +5 XP, за полный разгром босса — ачивка и бонус.

' - +secNav('p9',null); + +'
Карта связей понятий
Кликни по связи — увидишь, как понятия главы связаны.
'+secNav('p9',null); } /* ── монтаж sidebar после загрузки ачивки (движок сам строит) ── */