merge: feature/lab-content-engine → master
Контент-движок лаборатории (фазы 0-5): LabRegistry, data-driven регистрация, вынос тел в labs-bodies.html, ленивая загрузка кода, БД-каталог lab_sims + API + админка, курикулумные связи lab_sim_links + двусторонняя навигация. Плюс накопленная работа параллельных сессий (chemistry-8, phys7, biochem, optics). Разрешение конфликтов: frontend/lab.html — версия feature (контент-движок); opticsbench.js / seed_biochem_challenges.js / BIOCHEM_UPGRADE.md / biochem-pathways-plan.md — версия master (более свежая работа парал. сессий). Тесты: 160, 157 pass, 3 fail (pre-existing baseline auth.test.js). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,305 @@
|
|||||||
|
/* gen_chem8_skeletons.js — генерирует каркасы 7 глав «Химия 8» (Phase 0).
|
||||||
|
* Запуск: node backend/scripts/gen_chem8_skeletons.js
|
||||||
|
* Выход: frontend/textbooks/chemistry_8_intro.html, _ch1.html ... _ch6.html
|
||||||
|
*
|
||||||
|
* Каркас = валидная брендированная страница: header (водяной знак), hero,
|
||||||
|
* оглавление § (read-only), баннер «в разработке», ссылка назад в хаб, тема.
|
||||||
|
* Полный интерактивный SPA-контент каждой главы добавляется в Phase 1–6
|
||||||
|
* (файлы перезаписываются), пока скелет обеспечивает навигацию и структуру.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
|
||||||
|
|
||||||
|
const P = (t, n) => ({ t, n }); // параграф
|
||||||
|
const NOTE = (note) => ({ note }); // лаб. опыт / практическая работа
|
||||||
|
|
||||||
|
const CHAPTERS = [
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_intro.html', slug: 'chemistry-8-intro',
|
||||||
|
kicker: 'Вводный раздел', title: 'Количественные понятия в химии',
|
||||||
|
range: '§ 1–9', wm: 'mol',
|
||||||
|
color: { p:'#d97706', d:'#b45309', l:'#fbbf24', soft:'#fef3c7', bgd:'#1c1410', cardd:'#271c14', textd:'#fef3c7' },
|
||||||
|
items: [
|
||||||
|
P('§ 1', 'Атомы. Химические элементы. Относительная атомная масса'),
|
||||||
|
P('§ 2', 'Молекулы. Простые и сложные вещества. Химические формулы. Относительная молекулярная масса'),
|
||||||
|
P('§ 3', 'Химическое количество вещества'),
|
||||||
|
P('§ 4', 'Моль — единица химического количества вещества. Постоянная Авогадро'),
|
||||||
|
P('§ 5', 'Молярная масса. Молярный объём газов'),
|
||||||
|
P('§ 6', 'Вычисление химического количества вещества по его массе и массы вещества по его химическому количеству'),
|
||||||
|
P('§ 7', 'Вычисление химического количества газа по его объёму и объёма газа по его химическому количеству'),
|
||||||
|
NOTE('Практическая работа 1. Химическое количество вещества'),
|
||||||
|
P('§ 8', 'Химические реакции'),
|
||||||
|
P('§ 9', 'Количественные расчёты по уравнениям химических реакций')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch1.html', slug: 'chemistry-8-ch1',
|
||||||
|
kicker: 'Глава 1', title: 'Важнейшие классы неорганических соединений',
|
||||||
|
range: '§ 10–23', wm: 'OH',
|
||||||
|
color: { p:'#0d9488', d:'#0f766e', l:'#14b8a6', soft:'#ccfbf1', bgd:'#0c1a18', cardd:'#102825', textd:'#ccfbf1' },
|
||||||
|
items: [
|
||||||
|
P('§ 10', 'Оксиды. Состав и классификация оксидов'),
|
||||||
|
P('§ 11', 'Химические свойства оксидов'),
|
||||||
|
P('§ 12', 'Получение и применение оксидов'),
|
||||||
|
P('§ 13', 'Кислоты. Состав и классификация кислот'),
|
||||||
|
P('§ 14', 'Химические свойства кислот'),
|
||||||
|
P('§ 15', 'Получение и применение кислот'),
|
||||||
|
P('§ 16', 'Основания'),
|
||||||
|
P('§ 17', 'Химические свойства оснований'),
|
||||||
|
P('§ 18', 'Получение и применение оснований'),
|
||||||
|
NOTE('Лабораторный опыт 1. Получение нерастворимого основания'),
|
||||||
|
NOTE('Практическая работа 2. Изучение реакции нейтрализации'),
|
||||||
|
P('§ 19', 'Соли. Состав и классификация солей'),
|
||||||
|
P('§ 20', 'Химические свойства солей'),
|
||||||
|
NOTE('Лабораторный опыт 2. Взаимодействие растворов солей с металлами'),
|
||||||
|
P('§ 21', 'Получение и применение солей'),
|
||||||
|
P('§ 22', 'Взаимосвязь между классами основных неорганических веществ'),
|
||||||
|
NOTE('Практическая работа 3. Решение экспериментальных задач'),
|
||||||
|
P('§ 23', 'Решение расчётных задач по теме «Основные классы неорганических соединений»')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch2.html', slug: 'chemistry-8-ch2',
|
||||||
|
kicker: 'Глава 2', title: 'Периодический закон и периодическая система химических элементов',
|
||||||
|
range: '§ 24–28', wm: '№',
|
||||||
|
color: { p:'#4f46e5', d:'#4338ca', l:'#818cf8', soft:'#e0e7ff', bgd:'#12122b', cardd:'#1b1b3a', textd:'#e0e7ff' },
|
||||||
|
items: [
|
||||||
|
P('§ 24', 'Систематизация химических элементов'),
|
||||||
|
P('§ 25', 'Понятие об амфотерности'),
|
||||||
|
NOTE('Лабораторный опыт 3. Получение гидроксида цинка и изучение его амфотерных свойств'),
|
||||||
|
P('§ 26', 'Естественные семейства элементов'),
|
||||||
|
P('§ 27', 'Периодический закон Д. И. Менделеева'),
|
||||||
|
P('§ 28', 'Периодическая система химических элементов')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch3.html', slug: 'chemistry-8-ch3',
|
||||||
|
kicker: 'Глава 3', title: 'Строение атома и периодичность изменения свойств',
|
||||||
|
range: '§ 29–35', wm: 'e−',
|
||||||
|
color: { p:'#2563eb', d:'#1d4ed8', l:'#60a5fa', soft:'#dbeafe', bgd:'#0a1428', cardd:'#102137', textd:'#dbeafe' },
|
||||||
|
items: [
|
||||||
|
P('§ 29', 'Строение атома. Атомный номер химического элемента'),
|
||||||
|
P('§ 30', 'Массовое число атома. Нуклиды'),
|
||||||
|
P('§ 31', 'Изотопы. Явление радиоактивности'),
|
||||||
|
P('§ 32', 'Состояние электронов в атоме. Электронное облако. Атомная орбиталь'),
|
||||||
|
P('§ 33', 'Строение электронных оболочек атомов'),
|
||||||
|
P('§ 34', 'Периодичность изменения свойств атомов химических элементов'),
|
||||||
|
P('§ 35', 'Характеристика химического элемента по его положению в периодической системе')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch4.html', slug: 'chemistry-8-ch4',
|
||||||
|
kicker: 'Глава 4', title: 'Химическая связь',
|
||||||
|
range: '§ 36–41', wm: 'H₂O',
|
||||||
|
color: { p:'#059669', d:'#047857', l:'#34d399', soft:'#d1fae5', bgd:'#0a1a12', cardd:'#10271c', textd:'#d1fae5' },
|
||||||
|
items: [
|
||||||
|
P('§ 36', 'Природа химической связи'),
|
||||||
|
P('§ 37', 'Ковалентная связь'),
|
||||||
|
P('§ 38', 'Неполярная и полярная ковалентная связь. Электроотрицательность'),
|
||||||
|
NOTE('Лабораторный опыт 4. Составление моделей молекул'),
|
||||||
|
P('§ 39', 'Ионная связь'),
|
||||||
|
P('§ 40', 'Металлическая связь. Межмолекулярное взаимодействие'),
|
||||||
|
P('§ 41', 'Кристаллическое состояние вещества')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch5.html', slug: 'chemistry-8-ch5',
|
||||||
|
kicker: 'Глава 5', title: 'Окислительно-восстановительные реакции',
|
||||||
|
range: '§ 42–45', wm: 'O₂',
|
||||||
|
color: { p:'#ea580c', d:'#c2410c', l:'#fb923c', soft:'#ffedd5', bgd:'#1c1208', cardd:'#2a1c10', textd:'#ffedd5' },
|
||||||
|
items: [
|
||||||
|
P('§ 42', 'Степень окисления'),
|
||||||
|
P('§ 43', 'Процессы окисления и восстановления'),
|
||||||
|
P('§ 44', 'Окислительно-восстановительные реакции'),
|
||||||
|
P('§ 45', 'Окислительно-восстановительные реакции вокруг нас')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'chemistry_8_ch6.html', slug: 'chemistry-8-ch6',
|
||||||
|
kicker: 'Глава 6', title: 'Растворы',
|
||||||
|
range: '§ 46–52', wm: 'aq',
|
||||||
|
color: { p:'#0891b2', d:'#0e7490', l:'#22d3ee', soft:'#cffafe', bgd:'#08191c', cardd:'#10282d', textd:'#cffafe' },
|
||||||
|
items: [
|
||||||
|
P('§ 46', 'Смеси веществ'),
|
||||||
|
P('§ 47', 'Растворение веществ в воде'),
|
||||||
|
P('§ 48', 'Характеристики растворимости веществ'),
|
||||||
|
P('§ 49', 'Качественные характеристики состава растворов'),
|
||||||
|
P('§ 50', 'Количественные характеристики растворённых веществ. Массовая доля растворённого вещества'),
|
||||||
|
P('§ 51', 'Молярная концентрация растворённых веществ'),
|
||||||
|
NOTE('Практическая работа 4. Приготовление раствора с заданной массовой долей и молярной концентрацией'),
|
||||||
|
P('§ 52', 'Вода и растворы в жизни и деятельности человека')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/[&<>]/g, c => ({ '&':'&', '<':'<', '>':'>' }[c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function outlineHtml(items) {
|
||||||
|
return items.map(it => {
|
||||||
|
if (it.note) {
|
||||||
|
return ' <li class="ol-note"><span class="ol-note-ic">' +
|
||||||
|
'<svg viewBox="0 0 24 24"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>' +
|
||||||
|
'</span><span>' + esc(it.note) + '</span></li>';
|
||||||
|
}
|
||||||
|
return ' <li class="ol-para"><span class="ol-num">' + esc(it.t) + '</span><span class="ol-name">' + esc(it.n) + '</span></li>';
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageHtml(ch) {
|
||||||
|
const c = ch.color;
|
||||||
|
const wmHeader = ch.kicker.toUpperCase();
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · ${esc(ch.kicker)} · «${esc(ch.title)}»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#fffbeb; --card:#fff; --text:#1c1917; --muted:#78716c; --border:#e7e5e4;
|
||||||
|
--pri:${c.p}; --pri-d:${c.d}; --pri-l:${c.l}; --pri-soft:${c.soft};
|
||||||
|
--sh:0 4px 16px rgba(0,0,0,.06); --sh-h:0 12px 32px rgba(0,0,0,.12);
|
||||||
|
}
|
||||||
|
html.dark{ --bg:${c.bgd}; --card:${c.cardd}; --text:${c.textd}; --muted:#a8a29e; --border:#3a3026; --pri-soft:rgba(0,0,0,.2); }
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{min-height:100vh}
|
||||||
|
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
|
||||||
|
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,${c.d} 0%,${c.p} 55%,${c.l} 100%);color:#fff;padding:34px 24px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
|
||||||
|
.hdr::before{content:'${wmHeader}';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(4rem,13vw,10rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.13);line-height:1;pointer-events:none;user-select:none;z-index:0}
|
||||||
|
.hdr-inner{position:relative;z-index:1;max-width:1000px;margin:0 auto;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||||||
|
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.16);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
|
||||||
|
.hdr-back:hover{background:rgba(255,255,255,.26)}
|
||||||
|
.hdr-kicker{font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.14em;opacity:.85}
|
||||||
|
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.55rem;font-weight:900;letter-spacing:-.01em;line-height:1.25;margin-top:3px}
|
||||||
|
.hdr-side{margin-left:auto}
|
||||||
|
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.26)}
|
||||||
|
|
||||||
|
main{max-width:1000px;margin:0 auto;padding:28px 24px 60px}
|
||||||
|
|
||||||
|
.wip{display:flex;gap:14px;align-items:flex-start;background:linear-gradient(135deg,var(--pri-soft),rgba(0,0,0,.02));border:1.5px dashed var(--pri);border-radius:16px;padding:18px 20px;margin-bottom:26px}
|
||||||
|
.wip-ic{width:42px;height:42px;border-radius:11px;background:var(--pri);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.wip-ic svg{width:22px;height:22px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.wip h2{font-family:'Outfit',sans-serif;font-size:1.05rem;color:var(--pri-d);margin-bottom:4px}
|
||||||
|
html.dark .wip h2{color:var(--pri-l)}
|
||||||
|
.wip p{font-size:.9rem;color:var(--muted);line-height:1.55}
|
||||||
|
|
||||||
|
.ol-title{font-family:'Outfit',sans-serif;font-size:1.15rem;font-weight:800;margin:6px 0 14px;display:flex;align-items:center;gap:9px}
|
||||||
|
.ol-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.ol-list{list-style:none;background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;box-shadow:var(--sh)}
|
||||||
|
.ol-para,.ol-note{display:flex;gap:12px;align-items:baseline;padding:12px 18px;border-bottom:1px solid var(--border)}
|
||||||
|
.ol-list li:last-child{border-bottom:0}
|
||||||
|
.ol-num{flex-shrink:0;min-width:46px;font-weight:800;color:var(--pri);font-size:.92rem}
|
||||||
|
.ol-name{font-size:.94rem;color:var(--text)}
|
||||||
|
.ol-note{background:var(--pri-soft);align-items:center;gap:10px}
|
||||||
|
.ol-note-ic{display:inline-flex;color:var(--pri-d)}
|
||||||
|
html.dark .ol-note-ic{color:var(--pri-l)}
|
||||||
|
.ol-note-ic svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.ol-note span:last-child{font-size:.88rem;font-weight:600;color:var(--pri-d)}
|
||||||
|
html.dark .ol-note span:last-child{color:var(--pri-l)}
|
||||||
|
|
||||||
|
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-inner">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-back">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
К разделам
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<div class="hdr-kicker">${esc(ch.kicker)} · ${esc(ch.range)}</div>
|
||||||
|
<h1>${esc(ch.title)}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
||||||
|
<span id="theme-lab">Тёмная</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="wip">
|
||||||
|
<div class="wip-ic">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4l-6.3 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l6.3-6.3a4 4 0 0 0 5.4-5.4l-2.6 2.6-2-2 2.6-2.6z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Раздел в разработке</h2>
|
||||||
|
<p>Интерактивное наглядное наполнение этого раздела (теория, модели, симуляторы, тренажёры и боссы) добавляется поэтапно. Ниже — план параграфов раздела согласно учебнику.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="ol-title">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
|
||||||
|
Содержание раздела
|
||||||
|
</div>
|
||||||
|
<ul class="ol-list">
|
||||||
|
${outlineHtml(ch.items)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
Интерактивный учебник «Химия — 8 класс» · ${esc(ch.kicker)} · LearnSpace
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
const _TB_SLUG = '${ch.slug}';
|
||||||
|
(function(){
|
||||||
|
var saved = localStorage.getItem('chemistry8_theme') || localStorage.getItem('theme') || 'light';
|
||||||
|
if (saved === 'dark') document.documentElement.classList.add('dark');
|
||||||
|
var lab = document.getElementById('theme-lab');
|
||||||
|
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', function(){
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
var dark = document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem('chemistry8_theme', dark ? 'dark' : 'light');
|
||||||
|
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
||||||
|
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --force перезапишет уже существующие файлы; по умолчанию — пропускаем
|
||||||
|
// готовые (наполненные в фазах) страницы, чтобы не затереть контент.
|
||||||
|
const FORCE = process.argv.includes('--force');
|
||||||
|
let count = 0, skipped = 0;
|
||||||
|
for (const ch of CHAPTERS) {
|
||||||
|
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, 'written,', skipped, 'skipped');
|
||||||
@@ -8,6 +8,9 @@ function list(req, res) {
|
|||||||
let where = '1=1';
|
let where = '1=1';
|
||||||
if (subject) { where += ' AND t.subject_slug = ?'; args.push(subject); }
|
if (subject) { where += ' AND t.subject_slug = ?'; args.push(subject); }
|
||||||
if (role !== 'admin') { where += ' AND t.created_by = ?'; args.push(uid); }
|
if (role !== 'admin') { where += ' AND t.created_by = ?'; args.push(uid); }
|
||||||
|
// Экзаменационные варианты — это служебные строки в tests (см. import-exam9.js),
|
||||||
|
// не показываем их во вкладке «Тесты (шаблоны)» админки.
|
||||||
|
where += ' AND t.id NOT IN (SELECT test_id FROM exam9_variant_tests)';
|
||||||
|
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT t.id, t.title, t.subject_slug, t.description, t.created_at,
|
SELECT t.id, t.title, t.subject_slug, t.description, t.created_at,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
-- Chemistry 8 hub migration.
|
||||||
|
-- Creates chemistry-8 as a full hub textbook (intro + 6 chapters) in the style of physics-9:
|
||||||
|
-- chemistry-8 (hub, html_path = chemistry_8_hub.html)
|
||||||
|
-- chemistry-8-intro (Количественные понятия, §§1–9) → chemistry_8_intro.html
|
||||||
|
-- chemistry-8-ch1 (Важнейшие классы соединений, §§10–23) → chemistry_8_ch1.html
|
||||||
|
-- chemistry-8-ch2 (Периодический закон и ПСХЭ, §§24–28) → chemistry_8_ch2.html
|
||||||
|
-- chemistry-8-ch3 (Строение атома, §§29–35) → chemistry_8_ch3.html
|
||||||
|
-- chemistry-8-ch4 (Химическая связь, §§36–41) → chemistry_8_ch4.html
|
||||||
|
-- chemistry-8-ch5 (ОВР, §§42–45) → chemistry_8_ch5.html
|
||||||
|
-- chemistry-8-ch6 (Растворы, §§46–52) → chemistry_8_ch6.html
|
||||||
|
--
|
||||||
|
-- Source: Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н.,
|
||||||
|
-- «Химия 8», Народная асвета, 2018. Контент авторский (наш).
|
||||||
|
-- Author left empty per project policy.
|
||||||
|
|
||||||
|
-- 1. Insert the parent chemistry-8 hub row (does not exist yet in the catalog).
|
||||||
|
INSERT INTO textbooks
|
||||||
|
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||||
|
VALUES
|
||||||
|
('chemistry-8', 'chemistry', 8, 'Химия — 8 класс',
|
||||||
|
'',
|
||||||
|
'Полный курс химии за 8 класс: количественные понятия (моль, молярная масса и объём, расчёты по уравнениям), важнейшие классы неорганических соединений, периодический закон и строение атома, химическая связь, окислительно-восстановительные реакции, растворы. 7 разделов, 52 параграфа, 4 лабораторных опыта, 4 практические работы.',
|
||||||
|
'chemistry_8_hub.html', 52, 'amber', 8, 1, NULL);
|
||||||
|
|
||||||
|
-- 2. Insert the 7 children (intro section + 6 chapters).
|
||||||
|
INSERT INTO textbooks
|
||||||
|
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||||
|
VALUES
|
||||||
|
('chemistry-8-intro', 'chemistry', 8, 'Химия 8 · Количественные понятия в химии',
|
||||||
|
'',
|
||||||
|
'§§1–9: атомы и химические элементы, простые и сложные вещества, химическое количество вещества, моль и постоянная Авогадро, молярная масса и молярный объём газов, расчёты по массе/объёму и по уравнениям реакций. Практическая работа 1.',
|
||||||
|
'chemistry_8_intro.html', 9, 'amber', 1, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch1', 'chemistry', 8, 'Химия 8 · Важнейшие классы неорганических соединений',
|
||||||
|
'',
|
||||||
|
'§§10–23: оксиды, кислоты, основания и соли — состав, классификация, химические свойства, получение и применение; генетическая связь между классами. 2 лабораторных опыта, 2 практические работы.',
|
||||||
|
'chemistry_8_ch1.html', 14, 'teal', 2, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch2', 'chemistry', 8, 'Химия 8 · Периодический закон и периодическая система',
|
||||||
|
'',
|
||||||
|
'§§24–28: систематизация элементов, амфотерность, естественные семейства элементов, периодический закон Д. И. Менделеева и строение периодической системы. Лабораторный опыт 3.',
|
||||||
|
'chemistry_8_ch2.html', 5, 'indigo', 3, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch3', 'chemistry', 8, 'Химия 8 · Строение атома',
|
||||||
|
'',
|
||||||
|
'§§29–35: строение атома и атомный номер, массовое число и нуклиды, изотопы и радиоактивность, электронное облако и атомная орбиталь, строение электронных оболочек, периодичность свойств, характеристика элемента по положению в ПС.',
|
||||||
|
'chemistry_8_ch3.html', 7, 'blue', 4, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch4', 'chemistry', 8, 'Химия 8 · Химическая связь',
|
||||||
|
'',
|
||||||
|
'§§36–41: природа химической связи, ковалентная связь (неполярная и полярная, электроотрицательность), ионная и металлическая связь, межмолекулярное взаимодействие, кристаллическое состояние вещества. Лабораторный опыт 4.',
|
||||||
|
'chemistry_8_ch4.html', 6, 'green', 5, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch5', 'chemistry', 8, 'Химия 8 · Окислительно-восстановительные реакции',
|
||||||
|
'',
|
||||||
|
'§§42–45: степень окисления, процессы окисления и восстановления, окислительно-восстановительные реакции и метод электронного баланса, ОВР вокруг нас.',
|
||||||
|
'chemistry_8_ch5.html', 4, 'orange', 6, 1, 'chemistry-8'),
|
||||||
|
('chemistry-8-ch6', 'chemistry', 8, 'Химия 8 · Растворы',
|
||||||
|
'',
|
||||||
|
'§§46–52: смеси веществ, растворение веществ в воде, характеристики растворимости, качественные и количественные характеристики состава растворов, массовая доля и молярная концентрация, вода и растворы в жизни человека. Практическая работа 4.',
|
||||||
|
'chemistry_8_ch6.html', 7, 'cyan', 7, 1, 'chemistry-8');
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- 042_lab_sims.sql — Контент-движок лаборатории, Фаза 4.
|
||||||
|
-- Каталог симуляций в БД: метаданные + оверрайды (вкл/выкл, порядок, теги,
|
||||||
|
-- рекомендуемые, курикулумные subject/grade). Источник истины каталога для
|
||||||
|
-- админки и (опционально) для /lab. Превью-SVG остаются в коде (frontend).
|
||||||
|
--
|
||||||
|
-- Совместимость: вкл/выкл также зеркалится в app_settings.sim_disabled_ids
|
||||||
|
-- на уровне API, поэтому существующая логика lab.html не ломается.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS lab_sims (
|
||||||
|
id TEXT PRIMARY KEY, -- id симуляции ('pendulum', ...)
|
||||||
|
cat TEXT NOT NULL, -- math | phys | chem | bio | game
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
subject TEXT, -- курикулум (Фаза 5), напр. 'physics'
|
||||||
|
grade INTEGER, -- класс (Фаза 5)
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1, -- 0 = скрыта в каталоге
|
||||||
|
featured INTEGER NOT NULL DEFAULT 0, -- 1 = «рекомендуемая»
|
||||||
|
tags TEXT NOT NULL DEFAULT '[]', -- JSON-массив строк
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lab_sims_sort ON lab_sims (sort_order);
|
||||||
|
|
||||||
|
-- Сид 40 симуляций в текущем порядке каталога /lab (из frontend SIMS).
|
||||||
|
INSERT OR IGNORE INTO lab_sims (id, cat, title, sort_order) VALUES
|
||||||
|
('graph', 'math', 'График функции', 1),
|
||||||
|
('graphtransform', 'math', 'Трансформации графиков', 2),
|
||||||
|
('geometry', 'math', 'Планиметрия', 3),
|
||||||
|
('triangle', 'math', 'Геометрия треугольника', 4),
|
||||||
|
('quadratic', 'math', 'Корни квадратного уравнения', 5),
|
||||||
|
('stereo', 'math', 'Стереометрия 3D', 6),
|
||||||
|
('probability', 'math', 'Теория вероятностей', 7),
|
||||||
|
('trigcircle', 'math', 'Тригонометрическая окружность', 8),
|
||||||
|
('normaldist', 'math', 'Нормальное распределение', 9),
|
||||||
|
('projectile', 'phys', 'Бросок тела', 10),
|
||||||
|
('pendulum', 'phys', 'Маятник', 11),
|
||||||
|
('collision', 'phys', 'Столкновение шаров', 12),
|
||||||
|
('emfield', 'phys', 'Электромагнитные поля', 13),
|
||||||
|
('circuit', 'phys', 'Электрические цепи', 14),
|
||||||
|
('hydrostatics', 'phys', 'Гидростатика', 15),
|
||||||
|
('dynamics', 'phys', 'Динамика', 16),
|
||||||
|
('opticsbench', 'phys', 'Оптическая скамья', 17),
|
||||||
|
('isoprocess', 'phys', 'Изопроцессы', 18),
|
||||||
|
('waves', 'phys', 'Волны и звук', 19),
|
||||||
|
('radioactive', 'phys', 'Радиоактивный распад', 20),
|
||||||
|
('race', 'phys', 'Гонка с задачами', 21),
|
||||||
|
('heatengine', 'phys', 'Тепловые двигатели', 22),
|
||||||
|
('logic', 'phys', 'Логические схемы', 23),
|
||||||
|
('molphys', 'chem', 'Молекулярная физика', 24),
|
||||||
|
('chemistry', 'chem', 'Химические реакции', 25),
|
||||||
|
('equilibrium', 'chem', 'Химическое равновесие', 26),
|
||||||
|
('electrolysis', 'chem', 'Электролиз', 27),
|
||||||
|
('bohratom', 'chem', 'Атом Бора', 28),
|
||||||
|
('orbitals', 'chem', 'Молекулярные орбитали', 29),
|
||||||
|
('titration', 'chem', 'pH и кривая титрования', 30),
|
||||||
|
('chemsandbox', 'chem', 'Химическая песочница', 31),
|
||||||
|
('stoichiometry', 'chem', 'Стехиометрия', 32),
|
||||||
|
('crystal', 'chem', 'Кристаллическая решётка', 33),
|
||||||
|
('qualanalysis', 'chem', 'Качественный анализ', 34),
|
||||||
|
('periodic', 'chem', 'Периодическая таблица', 35),
|
||||||
|
('organic', 'chem', 'Органическая химия', 36),
|
||||||
|
('solutions', 'chem', 'Растворы', 37),
|
||||||
|
('celldivision', 'bio', 'Деление клетки', 38),
|
||||||
|
('photosynthesis', 'bio', 'Фотосинтез и дыхание', 39),
|
||||||
|
('angrybirds', 'game', 'Angry Birds Physics', 40);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- 043_lab_sim_links.sql — Контент-движок лаборатории, Фаза 5 (курикулумная привязка).
|
||||||
|
-- Связи симуляции с учебной программой: § учебника, тема, узел knowledge-map,
|
||||||
|
-- задача банка вопросов. Двусторонняя навигация (sim ↔ контент).
|
||||||
|
--
|
||||||
|
-- kind:
|
||||||
|
-- 'textbook' — ref_id = textbooks.slug
|
||||||
|
-- 'topic' — ref_id = topics.id (как текст)
|
||||||
|
-- 'kmap' — ref_id = id узла графа знаний (свободная строка)
|
||||||
|
-- 'question' — ref_id = questions.id (как текст)
|
||||||
|
-- label — необязательная человекочитаемая подпись (если не резолвится из БД).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS lab_sim_links (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sim_id TEXT NOT NULL,
|
||||||
|
kind TEXT NOT NULL, -- textbook | topic | kmap | question
|
||||||
|
ref_id TEXT NOT NULL,
|
||||||
|
label TEXT,
|
||||||
|
created_by INTEGER REFERENCES users(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE (sim_id, kind, ref_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lab_sim_links_sim ON lab_sim_links (sim_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lab_sim_links_ref ON lab_sim_links (kind, ref_id);
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
'use strict';
|
||||||
|
/* /api/lab — каталог симуляций лаборатории (контент-движок, Фазы 4-5).
|
||||||
|
*
|
||||||
|
* GET /api/lab/sims — каталог из БД (lab_sims) + legacy-флаги. auth.
|
||||||
|
* PATCH /api/lab/sims/:id — enabled/featured/tags/subject/grade. admin.
|
||||||
|
* POST /api/lab/sims/reorder — задать порядок (массив id). admin.
|
||||||
|
* GET /api/lab/sims/:id/related — связанные § / темы / kmap / задачи. auth. (Ф5)
|
||||||
|
* POST /api/lab/sims/:id/links — добавить связь. admin. (Ф5)
|
||||||
|
* DELETE /api/lab/sims/:id/links/:linkId — удалить связь. admin. (Ф5)
|
||||||
|
* GET /api/lab/links?kind=&ref_id= — обратный поиск: какие симуляции привязаны
|
||||||
|
* к данному учебнику/теме. auth. (Ф5)
|
||||||
|
*
|
||||||
|
* Совместимость: enabled зеркалится в app_settings.sim_disabled_ids, поэтому
|
||||||
|
* существующая логика lab.html (которая читает /api/settings/sims) продолжает
|
||||||
|
* корректно скрывать отключённые симуляции без правок фронта. */
|
||||||
|
const router = require('express').Router();
|
||||||
|
const db = require('../db/db');
|
||||||
|
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const CATS = ['math', 'phys', 'chem', 'bio', 'game'];
|
||||||
|
const LINK_KINDS = ['textbook', 'topic', 'kmap', 'question'];
|
||||||
|
|
||||||
|
router.use(authMiddleware);
|
||||||
|
|
||||||
|
/* ── helpers ───────────────────────────────────────────────────────────── */
|
||||||
|
function readModuleDisabled() {
|
||||||
|
const row = db.prepare(`SELECT value FROM app_settings WHERE key = 'sim_module_disabled'`).get();
|
||||||
|
return row ? row.value === '1' : false;
|
||||||
|
}
|
||||||
|
function readLegacyDisabledIds() {
|
||||||
|
const row = db.prepare(`SELECT value FROM app_settings WHERE key = 'sim_disabled_ids'`).get();
|
||||||
|
try { return new Set(JSON.parse(row && row.value || '[]')); } catch { return new Set(); }
|
||||||
|
}
|
||||||
|
function writeLegacyDisabledIds(set) {
|
||||||
|
db.prepare(`INSERT INTO app_settings (key, value) VALUES ('sim_disabled_ids', ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
||||||
|
.run(JSON.stringify([...set]));
|
||||||
|
}
|
||||||
|
function parseTags(raw) { try { return JSON.parse(raw || '[]'); } catch { return []; } }
|
||||||
|
|
||||||
|
function rowToSim(r) {
|
||||||
|
return {
|
||||||
|
id: r.id, cat: r.cat, title: r.title,
|
||||||
|
subject: r.subject || null, grade: r.grade != null ? r.grade : null,
|
||||||
|
sort: r.sort_order, enabled: !!r.enabled, featured: !!r.featured,
|
||||||
|
tags: parseTags(r.tags),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GET /api/lab/sims ─────────────────────────────────────────────────── */
|
||||||
|
router.get('/sims', (_req, res) => {
|
||||||
|
let rows;
|
||||||
|
try {
|
||||||
|
rows = db.prepare(`SELECT * FROM lab_sims ORDER BY sort_order, id`).all();
|
||||||
|
} catch (e) {
|
||||||
|
// Деградация вместо 500: если миграция lab_sims ещё не применена на этом
|
||||||
|
// инстансе (старый процесс/другая БД) — отдаём пустой каталог, чтобы админка
|
||||||
|
// не падала. Нужно применить миграцию и перезапустить сервер.
|
||||||
|
console.warn('[lab] lab_sims недоступна (нужна миграция/перезапуск):', e.message);
|
||||||
|
return res.json({ module_disabled: readModuleDisabled(), sims: [], needs_migration: true });
|
||||||
|
}
|
||||||
|
const legacyDisabled = readLegacyDisabledIds();
|
||||||
|
const sims = rows.map(r => {
|
||||||
|
const s = rowToSim(r);
|
||||||
|
// Симуляция считается выключенной, если так сказано в lab_sims ИЛИ в legacy-списке.
|
||||||
|
s.enabled = s.enabled && !legacyDisabled.has(r.id);
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
res.json({ module_disabled: readModuleDisabled(), sims });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── admin mutations ───────────────────────────────────────────────────────
|
||||||
|
ВАЖНО: НЕ используем blanket `router.use(requireRole('admin'))` — он применялся
|
||||||
|
бы и к ниже определённым READ-роутам Фазы 5 (/related, /links), которые должны
|
||||||
|
быть доступны любому авторизованному пользователю. Каждая мутация защищена
|
||||||
|
INLINE requireRole('admin') (так же видит route-auth линтер). */
|
||||||
|
|
||||||
|
/* PATCH /api/lab/sims/:id body: { enabled?, featured?, tags?, subject?, grade?, title?, cat? } */
|
||||||
|
router.patch('/sims/:id', requireRole('admin'), (req, res) => {
|
||||||
|
const id = String(req.params.id || '');
|
||||||
|
const row = db.prepare('SELECT * FROM lab_sims WHERE id = ?').get(id);
|
||||||
|
if (!row) return res.status(404).json({ error: 'симуляция не найдена' });
|
||||||
|
|
||||||
|
const b = req.body || {};
|
||||||
|
const sets = [];
|
||||||
|
const vals = [];
|
||||||
|
|
||||||
|
if (b.enabled !== undefined) { sets.push('enabled = ?'); vals.push(b.enabled ? 1 : 0); }
|
||||||
|
if (b.featured !== undefined) { sets.push('featured = ?'); vals.push(b.featured ? 1 : 0); }
|
||||||
|
if (b.title !== undefined) {
|
||||||
|
const t = String(b.title).trim();
|
||||||
|
if (!t) return res.status(400).json({ error: 'пустой title' });
|
||||||
|
sets.push('title = ?'); vals.push(t);
|
||||||
|
}
|
||||||
|
if (b.cat !== undefined) {
|
||||||
|
if (!CATS.includes(b.cat)) return res.status(400).json({ error: 'неверная категория' });
|
||||||
|
sets.push('cat = ?'); vals.push(b.cat);
|
||||||
|
}
|
||||||
|
if (b.subject !== undefined) { sets.push('subject = ?'); vals.push(b.subject ? String(b.subject) : null); }
|
||||||
|
if (b.grade !== undefined) {
|
||||||
|
const g = b.grade === null || b.grade === '' ? null : Number(b.grade);
|
||||||
|
if (g !== null && (!Number.isInteger(g) || g < 1 || g > 11)) {
|
||||||
|
return res.status(400).json({ error: 'grade должен быть 1..11 или null' });
|
||||||
|
}
|
||||||
|
sets.push('grade = ?'); vals.push(g);
|
||||||
|
}
|
||||||
|
if (b.tags !== undefined) {
|
||||||
|
if (!Array.isArray(b.tags)) return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||||
|
const clean = b.tags.map(t => String(t).trim()).filter(Boolean).slice(0, 20);
|
||||||
|
sets.push('tags = ?'); vals.push(JSON.stringify(clean));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sets.length) return res.status(400).json({ error: 'нет полей для обновления' });
|
||||||
|
|
||||||
|
sets.push("updated_at = datetime('now')");
|
||||||
|
vals.push(id);
|
||||||
|
db.prepare(`UPDATE lab_sims SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
||||||
|
|
||||||
|
// Зеркалим enabled в legacy sim_disabled_ids для совместимости с lab.html.
|
||||||
|
if (b.enabled !== undefined) {
|
||||||
|
const set = readLegacyDisabledIds();
|
||||||
|
if (b.enabled) set.delete(id); else set.add(id);
|
||||||
|
writeLegacyDisabledIds(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = db.prepare('SELECT * FROM lab_sims WHERE id = ?').get(id);
|
||||||
|
res.json({ ok: true, sim: rowToSim(updated) });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* POST /api/lab/sims/reorder body: { order: [id, id, ...] } */
|
||||||
|
router.post('/sims/reorder', requireRole('admin'), (req, res) => {
|
||||||
|
const order = (req.body && req.body.order) || [];
|
||||||
|
if (!Array.isArray(order) || !order.length) {
|
||||||
|
return res.status(400).json({ error: 'order должен быть непустым массивом id' });
|
||||||
|
}
|
||||||
|
const exists = new Set(db.prepare('SELECT id FROM lab_sims').all().map(r => r.id));
|
||||||
|
for (const id of order) {
|
||||||
|
if (!exists.has(id)) return res.status(400).json({ error: 'неизвестный id: ' + id });
|
||||||
|
}
|
||||||
|
const upd = db.prepare("UPDATE lab_sims SET sort_order = ?, updated_at = datetime('now') WHERE id = ?");
|
||||||
|
db.transaction(() => {
|
||||||
|
order.forEach((id, i) => upd.run(i + 1, id));
|
||||||
|
})();
|
||||||
|
res.json({ ok: true, count: order.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
|
Курикулумная привязка (Фаза 5) — связи симуляции ↔ контент.
|
||||||
|
════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
// Безопасно прочитать связи симуляции (если таблицы ещё нет — пустой массив).
|
||||||
|
function readLinks(simId) {
|
||||||
|
try {
|
||||||
|
return db.prepare(
|
||||||
|
'SELECT id, sim_id, kind, ref_id, label FROM lab_sim_links WHERE sim_id = ? ORDER BY kind, id'
|
||||||
|
).all(simId);
|
||||||
|
} catch (e) {
|
||||||
|
return null; // null => таблица недоступна (нужна миграция)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обогатить связь человекочитаемой меткой и навигационным href.
|
||||||
|
function decorateLink(l) {
|
||||||
|
const out = { id: l.id, kind: l.kind, ref_id: l.ref_id, label: l.label || null };
|
||||||
|
if (l.kind === 'textbook') {
|
||||||
|
const tb = db.prepare('SELECT title, subject, grade FROM textbooks WHERE slug = ?').get(l.ref_id);
|
||||||
|
if (tb) { out.label = out.label || tb.title; out.subject = tb.subject; out.grade = tb.grade; }
|
||||||
|
out.href = '/textbooks?book=' + encodeURIComponent(l.ref_id);
|
||||||
|
} else if (l.kind === 'topic') {
|
||||||
|
const tp = db.prepare('SELECT name FROM topics WHERE id = ?').get(Number(l.ref_id));
|
||||||
|
if (tp) out.label = out.label || tp.name;
|
||||||
|
} else if (l.kind === 'question') {
|
||||||
|
out.href = null; // задачи открываются в банке вопросов отдельным контекстом
|
||||||
|
}
|
||||||
|
if (!out.label) out.label = l.kind + ':' + l.ref_id;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GET /api/lab/sims/:id/related → { sim, links:{ textbook:[], topic:[], kmap:[], question:[] } } */
|
||||||
|
router.get('/sims/:id/related', authMiddleware, (req, res) => {
|
||||||
|
const id = String(req.params.id || '');
|
||||||
|
const sim = db.prepare('SELECT id, title FROM lab_sims WHERE id = ?').get(id);
|
||||||
|
// sim может отсутствовать в lab_sims (если миграция 042 не применена) — не 404,
|
||||||
|
// т.к. связи всё равно могут существовать; вернём то, что есть.
|
||||||
|
const rows = readLinks(id);
|
||||||
|
if (rows === null) return res.json({ sim: sim || { id }, links: {}, needs_migration: true });
|
||||||
|
const links = { textbook: [], topic: [], kmap: [], question: [] };
|
||||||
|
for (const l of rows) {
|
||||||
|
const d = decorateLink(l);
|
||||||
|
(links[l.kind] || (links[l.kind] = [])).push(d);
|
||||||
|
}
|
||||||
|
res.json({ sim: sim || { id }, links });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* GET /api/lab/links?kind=textbook&ref_id=algebra-8
|
||||||
|
→ { sims:[{id,title,cat,enabled}] } — какие (включённые) симуляции привязаны. */
|
||||||
|
router.get('/links', (req, res) => {
|
||||||
|
const kind = String(req.query.kind || '');
|
||||||
|
const refId = String(req.query.ref_id || '');
|
||||||
|
if (!LINK_KINDS.includes(kind) || !refId) {
|
||||||
|
return res.status(400).json({ error: 'kind и ref_id обязательны' });
|
||||||
|
}
|
||||||
|
let rows;
|
||||||
|
try {
|
||||||
|
rows = db.prepare(`
|
||||||
|
SELECT s.id, s.title, s.cat, s.enabled
|
||||||
|
FROM lab_sim_links l JOIN lab_sims s ON s.id = l.sim_id
|
||||||
|
WHERE l.kind = ? AND l.ref_id = ?
|
||||||
|
ORDER BY s.sort_order, s.id
|
||||||
|
`).all(kind, refId);
|
||||||
|
} catch (e) {
|
||||||
|
return res.json({ sims: [], needs_migration: true });
|
||||||
|
}
|
||||||
|
const legacyDisabled = readLegacyDisabledIds();
|
||||||
|
const sims = rows
|
||||||
|
.map(r => ({ id: r.id, title: r.title, cat: r.cat, enabled: !!r.enabled && !legacyDisabled.has(r.id) }))
|
||||||
|
.filter(s => s.enabled); // наружу отдаём только доступные
|
||||||
|
res.json({ sims });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* GET /api/lab/links/all?kind=textbook
|
||||||
|
→ { byRef: { <ref_id>: [{id,title,cat}] } } — пакетный обратный поиск для всех
|
||||||
|
ref_id данного типа за один запрос (избегаем N+1 на странице каталога учебников).
|
||||||
|
Отдаёт только включённые симуляции. */
|
||||||
|
router.get('/links/all', (req, res) => {
|
||||||
|
const kind = String(req.query.kind || '');
|
||||||
|
if (!LINK_KINDS.includes(kind)) {
|
||||||
|
return res.status(400).json({ error: 'неверный kind' });
|
||||||
|
}
|
||||||
|
let rows;
|
||||||
|
try {
|
||||||
|
rows = db.prepare(`
|
||||||
|
SELECT l.ref_id, s.id, s.title, s.cat, s.enabled, s.sort_order
|
||||||
|
FROM lab_sim_links l JOIN lab_sims s ON s.id = l.sim_id
|
||||||
|
WHERE l.kind = ?
|
||||||
|
ORDER BY s.sort_order, s.id
|
||||||
|
`).all(kind);
|
||||||
|
} catch (e) {
|
||||||
|
return res.json({ byRef: {}, needs_migration: true });
|
||||||
|
}
|
||||||
|
const legacyDisabled = readLegacyDisabledIds();
|
||||||
|
const byRef = {};
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!r.enabled || legacyDisabled.has(r.id)) continue;
|
||||||
|
(byRef[r.ref_id] || (byRef[r.ref_id] = [])).push({ id: r.id, title: r.title, cat: r.cat });
|
||||||
|
}
|
||||||
|
res.json({ byRef });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── admin: управление связями ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* POST /api/lab/sims/:id/links body: { kind, ref_id, label? } */
|
||||||
|
router.post('/sims/:id/links', requireRole('admin'), (req, res) => {
|
||||||
|
const simId = String(req.params.id || '');
|
||||||
|
if (!db.prepare('SELECT 1 FROM lab_sims WHERE id = ?').get(simId)) {
|
||||||
|
return res.status(404).json({ error: 'симуляция не найдена' });
|
||||||
|
}
|
||||||
|
const b = req.body || {};
|
||||||
|
const kind = String(b.kind || '');
|
||||||
|
const refId = String(b.ref_id || '').trim();
|
||||||
|
if (!LINK_KINDS.includes(kind)) return res.status(400).json({ error: 'неверный kind' });
|
||||||
|
if (!refId) return res.status(400).json({ error: 'ref_id обязателен' });
|
||||||
|
|
||||||
|
// Валидация существования цели (мягкая — kmap/question произвольны).
|
||||||
|
if (kind === 'textbook' && !db.prepare('SELECT 1 FROM textbooks WHERE slug = ?').get(refId)) {
|
||||||
|
return res.status(404).json({ error: 'учебник не найден: ' + refId });
|
||||||
|
}
|
||||||
|
if (kind === 'topic') {
|
||||||
|
const tid = Number(refId);
|
||||||
|
if (!Number.isInteger(tid) || !db.prepare('SELECT 1 FROM topics WHERE id = ?').get(tid)) {
|
||||||
|
return res.status(404).json({ error: 'тема не найдена: ' + refId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = b.label != null ? String(b.label).trim().slice(0, 200) || null : null;
|
||||||
|
try {
|
||||||
|
const info = db.prepare(
|
||||||
|
'INSERT INTO lab_sim_links (sim_id, kind, ref_id, label, created_by) VALUES (?, ?, ?, ?, ?)'
|
||||||
|
).run(simId, kind, refId, label, req.user.id);
|
||||||
|
const created = db.prepare('SELECT id, sim_id, kind, ref_id, label FROM lab_sim_links WHERE id = ?')
|
||||||
|
.get(info.lastInsertRowid);
|
||||||
|
res.json({ ok: true, link: decorateLink(created) });
|
||||||
|
} catch (e) {
|
||||||
|
if (/UNIQUE/i.test(e.message)) return res.status(409).json({ error: 'такая связь уже есть' });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* DELETE /api/lab/sims/:id/links/:linkId */
|
||||||
|
router.delete('/sims/:id/links/:linkId', requireRole('admin'), (req, res) => {
|
||||||
|
const simId = String(req.params.id || '');
|
||||||
|
const linkId = Number(req.params.linkId);
|
||||||
|
if (!Number.isInteger(linkId)) return res.status(400).json({ error: 'неверный linkId' });
|
||||||
|
const info = db.prepare('DELETE FROM lab_sim_links WHERE id = ? AND sim_id = ?').run(linkId, simId);
|
||||||
|
if (!info.changes) return res.status(404).json({ error: 'связь не найдена' });
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -55,6 +55,7 @@ const examPrepRoutes = require('./routes/exam-prep');
|
|||||||
const textbookRoutes = require('./routes/textbooks');
|
const textbookRoutes = require('./routes/textbooks');
|
||||||
const accessRoutes = require('./routes/access');
|
const accessRoutes = require('./routes/access');
|
||||||
const teacherStudentsRoutes = require('./routes/teacherStudents');
|
const teacherStudentsRoutes = require('./routes/teacherStudents');
|
||||||
|
const labRoutes = require('./routes/lab');
|
||||||
|
|
||||||
const { requestId, errorHandler } = require('./middleware/errorHandler');
|
const { requestId, errorHandler } = require('./middleware/errorHandler');
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ app.use('/api/exam-prep', examPrepRoutes);
|
|||||||
app.use('/api/textbooks', textbookRoutes);
|
app.use('/api/textbooks', textbookRoutes);
|
||||||
app.use('/api/access', accessRoutes);
|
app.use('/api/access', accessRoutes);
|
||||||
app.use('/api/teacher-students', teacherStudentsRoutes);
|
app.use('/api/teacher-students', teacherStudentsRoutes);
|
||||||
|
app.use('/api/lab', labRoutes);
|
||||||
|
|
||||||
/* ── Public features endpoint (merges global + per-class for authenticated students) ── */
|
/* ── Public features endpoint (merges global + per-class for authenticated students) ── */
|
||||||
const _featDb = require('./db/db');
|
const _featDb = require('./db/db');
|
||||||
|
|||||||
@@ -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('<!DOCTYPE html><body><div id="m"></div><div id="b"></div></body>');
|
||||||
|
// выполняем модуль так, что его `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 сбалансировано');
|
||||||
|
});
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* Полностраничная jsdom-проверка глав «Химия 8» (SPA на chem8_engine.js):
|
||||||
|
* выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем
|
||||||
|
* para-selector, активный §, монтаж виджетов — без ошибок скриптов.
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { JSDOM, VirtualConsole } = require('jsdom');
|
||||||
|
|
||||||
|
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(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_mol.js': readF('frontend/js/chem8_mol.js'),
|
||||||
|
[widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/js', '')),
|
||||||
|
'/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
|
||||||
|
};
|
||||||
|
html = html
|
||||||
|
.replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
|
||||||
|
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
|
||||||
|
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
|
||||||
|
Object.keys(inl).forEach(src => {
|
||||||
|
html = html.replace(new RegExp('<script src="' + src + '" defer><\\/script>'), () => '<script>\n' + inl[src] + '\n</script>');
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDom(file, widgetsSrc) {
|
||||||
|
const errors = [];
|
||||||
|
const vc = new VirtualConsole();
|
||||||
|
vc.on('jsdomError', e => errors.push(e.message));
|
||||||
|
const dom = new JSDOM(buildPage(file, widgetsSrc), {
|
||||||
|
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
||||||
|
beforeParse(w) { w.scrollTo = function () {}; }
|
||||||
|
});
|
||||||
|
await wait(180);
|
||||||
|
return { dom, errors, doc: dom.window.document };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Вводный раздел ── */
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глава 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('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');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ch1: генетическая карта §22 монтируется (U3)', async () => {
|
||||||
|
const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
|
||||||
|
doc.defaultView.goTo('p22'); await wait(120);
|
||||||
|
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');
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 6, '5 § + финал');
|
||||||
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p24', '§24 активен');
|
||||||
|
await wait(120);
|
||||||
|
assert.ok(doc.querySelectorAll('#c-pt-metals .pt-cell').length > 80, 'ПСХЭ §24 (90 элементов)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ch2: амфотерность §25 и семейства §26 монтируются', async () => {
|
||||||
|
const { doc } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
|
||||||
|
doc.defaultView.goTo('p25'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-amph .amph-btn'), 'амфотерность §25');
|
||||||
|
doc.defaultView.goTo('p26'); await wait(120);
|
||||||
|
assert.ok(doc.querySelectorAll('#c-pt-fam .pt-cell').length > 80, 'ПСХЭ семейства §26');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глава 3 ── */
|
||||||
|
test('ch3: SPA без ошибок, 8 карточек, §29 активен, модель атома', async () => {
|
||||||
|
const { doc, errors } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 8, '7 § + финал');
|
||||||
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p29', '§29 активен');
|
||||||
|
await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-atom .as-svg'), 'модель атома §29');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ch3: нуклид §30 и паспорт §35 монтируются', async () => {
|
||||||
|
const { doc } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
|
||||||
|
doc.defaultView.goTo('p30'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-nuclide #nz'), 'калькулятор нуклида §30');
|
||||||
|
doc.defaultView.goTo('p35'); await wait(120);
|
||||||
|
assert.ok(doc.querySelectorAll('#c-passport .pt-cell').length > 80, 'ПСХЭ паспорта §35');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глава 4 ── */
|
||||||
|
test('ch4: SPA без ошибок, 7 карточек, §36 активен, тип связи', async () => {
|
||||||
|
const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 7, '6 § + финал');
|
||||||
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p36', '§36 активен');
|
||||||
|
doc.defaultView.goTo('p37'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-bond1 .bt-svg'), 'виджет типа связи §37');
|
||||||
|
doc.defaultView.goTo('p38'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-bond2 .bt-out'), 'виджет полярности §38');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ch4: 3D-модели молекул §38 и решётки §41 монтируются (U4)', async () => {
|
||||||
|
const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
doc.defaultView.goTo('p38'); await wait(140);
|
||||||
|
assert.ok(doc.querySelector('#c-mol .mol-sel'), 'выбор молекулы §38');
|
||||||
|
assert.ok(doc.querySelector('#c-mol canvas'), 'canvas 3D-модели §38');
|
||||||
|
assert.ok(doc.querySelector('#c-mol .mol-info'), 'инфо о молекуле §38');
|
||||||
|
doc.defaultView.goTo('p41'); await wait(140);
|
||||||
|
assert.ok(doc.querySelector('#c-lattice .lat-sel'), 'выбор решётки §41');
|
||||||
|
assert.ok(doc.querySelector('#c-lattice canvas'), 'canvas решётки §41');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глава 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глоссарий (U2/Phase 8) ── */
|
||||||
|
test('glossary: кнопка, модалка, авто-подсветка терминов', async () => {
|
||||||
|
const src = readF('frontend/js/chem8_glossary.js');
|
||||||
|
const dom = new JSDOM('<!DOCTYPE html><body><div class="card-body"><p>Оксид — это сложное вещество. Кислота реагирует с основанием в реакции нейтрализации.</p></div></body>',
|
||||||
|
{ runScripts: 'outside-only', pretendToBeVisual: true, url: 'http://localhost/' });
|
||||||
|
new Function('window', src)(dom.window);
|
||||||
|
await wait(20);
|
||||||
|
const doc = dom.window.document;
|
||||||
|
assert.ok(dom.window.Chem8Glossary, 'window.Chem8Glossary определён');
|
||||||
|
assert.ok(Object.keys(dom.window.Chem8Glossary.terms).length > 40, '>40 терминов');
|
||||||
|
assert.ok(doc.querySelector('.gl-fab'), 'плавающая кнопка глоссария');
|
||||||
|
// авто-подсветка терминов в .card-body
|
||||||
|
assert.ok(doc.querySelectorAll('.card-body .gloss').length >= 2, 'термины подсвечены: ' + doc.querySelectorAll('.gloss').length);
|
||||||
|
// открытие модалки
|
||||||
|
dom.window.Chem8Glossary.open();
|
||||||
|
assert.ok(doc.querySelector('.gl-modal.show'), 'модалка открыта');
|
||||||
|
assert.ok(doc.querySelectorAll('.gl-modal .gl-item').length > 40, 'список терминов в модалке');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Хаб: финал курса (Phase 7) ── */
|
||||||
|
function buildHub() {
|
||||||
|
let html = readF('frontend/textbooks/chemistry_8_hub.html');
|
||||||
|
return html
|
||||||
|
.replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
|
||||||
|
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
|
||||||
|
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
|
||||||
|
}
|
||||||
|
async function loadHub() {
|
||||||
|
const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message));
|
||||||
|
const dom = new JSDOM(buildHub(), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w){ w.scrollTo=function(){}; } });
|
||||||
|
await wait(60);
|
||||||
|
return { dom, errors, doc: dom.window.document };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('hub: финал курса — 10 боссов рендерятся при раскрытии, босс решается', async () => {
|
||||||
|
const { doc, errors } = await loadHub();
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 7, '7 карточек глав');
|
||||||
|
// раскрыть финал
|
||||||
|
doc.getElementById('final-head').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||||
|
await wait(40);
|
||||||
|
assert.equal(doc.querySelectorAll('#fin-bosses-container .boss-card').length, 10, '10 боссов');
|
||||||
|
// решить босс 1 (Mr Ca(OH)2 = 74)
|
||||||
|
const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
|
||||||
|
inp.value = '74'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||||
|
assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Глава 6 ── */
|
||||||
|
test('ch6: SPA без ошибок, 8 карточек, §46 активен, w/c калькуляторы', async () => {
|
||||||
|
const { doc, errors } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
|
||||||
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 8, '7 § + финал');
|
||||||
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p46', '§46 активен');
|
||||||
|
await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-mix .cls-chip'), 'классификатор смесей §46');
|
||||||
|
doc.defaultView.goTo('p50'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-wcalc #w-go'), 'калькулятор w §50');
|
||||||
|
doc.defaultView.goTo('p51'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-ccalc #c-go'), 'калькулятор c §51');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ch6: анимация растворения §47 монтируется (U3)', async () => {
|
||||||
|
const { doc } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
|
||||||
|
doc.defaultView.goTo('p47'); await wait(120);
|
||||||
|
assert.ok(doc.querySelector('#c-dissoc .ds-svg'), 'анимация диссоциации §47');
|
||||||
|
});
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* Phase 0 тесты учебника «Химия 8» (hub + 7 глав).
|
||||||
|
* 1. Чистые примитивы frontend/js/chem8_svg.js (window.Chem8): formula/ionLabel/chemEq.
|
||||||
|
* 2. Целостность каркаса: хаб + 7 файлов глав существуют, slug'и согласованы,
|
||||||
|
* сумма параграфов = 52, миграция 041 содержит родителя + 7 детей.
|
||||||
|
*/
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..', '..');
|
||||||
|
const TB = path.join(ROOT, 'frontend', 'textbooks');
|
||||||
|
|
||||||
|
// --- shim browser global, load the frontend module ---
|
||||||
|
global.window = global;
|
||||||
|
require(path.join(ROOT, 'frontend', 'js', 'chem8_svg.js'));
|
||||||
|
const C = global.Chem8;
|
||||||
|
|
||||||
|
test('Chem8.formula — числовые индексы в подстрочные', () => {
|
||||||
|
assert.equal(C.formula('CaCO3'), 'CaCO₃');
|
||||||
|
assert.equal(C.formula('H2O'), 'H₂O');
|
||||||
|
assert.equal(C.formula('Al2(SO4)3'), 'Al₂(SO₄)₃');
|
||||||
|
assert.equal(C.formula('NaCl'), 'NaCl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chem8.ionLabel — заряд ионов надстрочным', () => {
|
||||||
|
assert.equal(C.ionLabel('Na', 1), 'Na⁺');
|
||||||
|
assert.equal(C.ionLabel('Ca', 2), 'Ca²⁺');
|
||||||
|
assert.equal(C.ionLabel('Cl', -1), 'Cl⁻');
|
||||||
|
assert.equal(C.ionLabel('SO4', -2), 'SO₄²⁻');
|
||||||
|
assert.equal(C.ionLabel('Fe', 3), 'Fe³⁺');
|
||||||
|
assert.equal(C.ionLabel('Na', 0), 'Na');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chem8.chemEq — стрелка, индексы, газ', () => {
|
||||||
|
const html = C.chemEq('2Na + 2H2O -> 2NaOH + H2^');
|
||||||
|
assert.ok(html.includes('2H₂O'), 'индексы воды');
|
||||||
|
assert.ok(html.includes('→'), 'стрелка реакции');
|
||||||
|
assert.ok(html.includes('H₂↑'), 'значок газа');
|
||||||
|
assert.ok(html.includes('class="ceq"'), 'обёртка');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chem8.chemEq — обратимая реакция и осадок', () => {
|
||||||
|
const rev = C.chemEq('N2 + 3H2 <-> 2NH3');
|
||||||
|
assert.ok(rev.includes('⇌'), 'обратимая стрелка');
|
||||||
|
const prec = C.chemEq('AgNO3 + NaCl -> AgClv + NaNO3');
|
||||||
|
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 ['redoxBalancer', 'orbitalDiagram']) {
|
||||||
|
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('<svg'), 'testTube → SVG');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 },
|
||||||
|
{ slug: 'chemistry-8-ch1', file: 'chemistry_8_ch1.html', paras: 14 },
|
||||||
|
{ slug: 'chemistry-8-ch2', file: 'chemistry_8_ch2.html', paras: 5 },
|
||||||
|
{ slug: 'chemistry-8-ch3', file: 'chemistry_8_ch3.html', paras: 7 },
|
||||||
|
{ slug: 'chemistry-8-ch4', file: 'chemistry_8_ch4.html', paras: 6 },
|
||||||
|
{ slug: 'chemistry-8-ch5', file: 'chemistry_8_ch5.html', paras: 4 },
|
||||||
|
{ slug: 'chemistry-8-ch6', file: 'chemistry_8_ch6.html', paras: 7 }
|
||||||
|
];
|
||||||
|
|
||||||
|
test('сумма параграфов глав = 52', () => {
|
||||||
|
assert.equal(CHILDREN.reduce((a, c) => a + c.paras, 0), 52);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('хаб chemistry_8_hub.html существует и ссылается на все 7 глав', () => {
|
||||||
|
const hub = fs.readFileSync(path.join(TB, 'chemistry_8_hub.html'), 'utf8');
|
||||||
|
assert.ok(hub.includes('var TOTAL = 52'), 'TOTAL=52');
|
||||||
|
for (const ch of CHILDREN) {
|
||||||
|
assert.ok(hub.includes('/textbook/' + ch.slug), 'ссылка на ' + ch.slug);
|
||||||
|
}
|
||||||
|
assert.ok(hub.includes('/api/textbooks/chemistry-8/children'), 'грузит детей');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('каждая глава существует, ссылается на хаб и подключает chem8', () => {
|
||||||
|
for (const ch of CHILDREN) {
|
||||||
|
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');
|
||||||
|
// все 8 страниц (intro + 6 глав) перестроены на движок chem8_engine.js (SPA)
|
||||||
|
assert.ok(html.includes("slug:'" + ch.slug + "'"), ch.file + ' slug в CHEM8_CFG');
|
||||||
|
assert.ok(html.includes('/js/chem8_engine.js'), ch.file + ' подключает движок');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Phase 1 — раздел intro перестроен на движок (SPA, эталон)', () => {
|
||||||
|
const html = fs.readFileSync(path.join(TB, 'chemistry_8_intro.html'), 'utf8');
|
||||||
|
assert.ok(html.includes('id="psel-grid"'), 'para-selector');
|
||||||
|
for (let i = 1; i <= 9; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||||
|
assert.ok(html.includes('id="sec-pr1"'), 'ПР1 секция');
|
||||||
|
assert.ok(html.includes('id="sec-final1"'), 'финал-секция');
|
||||||
|
assert.ok(html.includes('window.POOLS'), 'тренажёр задач (POOLS)');
|
||||||
|
assert.ok(html.includes('window.BUILDERS'), 'builders §');
|
||||||
|
assert.ok(html.includes('function build_p6'), 'build_p6 (треугольник)');
|
||||||
|
assert.ok(html.includes('/css/chem8-textbook.css'), 'фреймворк-CSS');
|
||||||
|
assert.ok(html.includes('/js/chem8_intro_widgets.js'), 'виджеты раздела');
|
||||||
|
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('Phase 3 — Глава 2 построена на движке (§24–28 + Лаб.3 + финал)', () => {
|
||||||
|
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch2.html'), 'utf8');
|
||||||
|
for (let i = 24; i <= 28; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||||
|
assert.ok(html.includes('id="c-pt-metals"'), 'ПСХЭ §24');
|
||||||
|
assert.ok(html.includes('id="c-amph"'), 'амфотерность §25');
|
||||||
|
assert.ok(html.includes('Лабораторный опыт 3'), 'Лаб.3');
|
||||||
|
assert.ok(html.includes('/js/chem8_ch2_widgets.js'), 'виджеты главы 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chem8.miniPeriodic возвращает API с highlight', () => {
|
||||||
|
assert.equal(typeof C.miniPeriodic, 'function', 'miniPeriodic реализован');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Phase 4 — Глава 3 построена + atomShell/shellConfig корректны', () => {
|
||||||
|
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch3.html'), 'utf8');
|
||||||
|
for (let i = 29; i <= 35; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||||
|
assert.ok(html.includes('id="c-atom"'), 'модель атома §29');
|
||||||
|
assert.ok(html.includes('id="c-passport"'), 'паспорт §35');
|
||||||
|
assert.ok(html.includes('/js/chem8_ch3_widgets.js'), 'виджеты главы 3');
|
||||||
|
assert.deepEqual(C.shellConfig(11), [2, 8, 1], 'Na: 2,8,1');
|
||||||
|
assert.deepEqual(C.shellConfig(20), [2, 8, 8, 2], 'Ca: 2,8,8,2');
|
||||||
|
assert.equal(C.nuclide(11, 23).N, 12, '²³Na: 12 нейтронов');
|
||||||
|
assert.equal(C.zSym(17), 'Cl', 'Z=17 → Cl');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Phase 5 — Глава 4 построена + bondType корректен', () => {
|
||||||
|
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch4.html'), 'utf8');
|
||||||
|
for (let i = 36; i <= 41; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||||
|
assert.ok(html.includes('id="c-bond1"'), 'тип связи §37');
|
||||||
|
assert.ok(html.includes('Лабораторный опыт 4'), 'Лаб.4');
|
||||||
|
assert.ok(html.includes('/js/chem8_ch4_widgets.js'), 'виджеты главы 4');
|
||||||
|
assert.equal(C.bondClass('H', 'H').type, 'ковалентная неполярная');
|
||||||
|
assert.equal(C.bondClass('H', 'Cl').type, 'ковалентная полярная');
|
||||||
|
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('Phase 6 — Глава 6 построена (§46–52 + ПР4 + финал)', () => {
|
||||||
|
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch6.html'), 'utf8');
|
||||||
|
for (let i = 46; i <= 52; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||||
|
assert.ok(html.includes('id="c-mix"'), 'классификатор смесей §46');
|
||||||
|
assert.ok(html.includes('id="c-wcalc"'), 'калькулятор w §50');
|
||||||
|
assert.ok(html.includes('id="c-ccalc"'), 'калькулятор c §51');
|
||||||
|
assert.ok(html.includes('Практическая работа 4'), 'ПР4');
|
||||||
|
assert.ok(html.includes('/js/chem8_ch6_widgets.js'), 'виджеты главы 6');
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
assert.doesNotThrow(() => new Function(eng), 'движок парсится');
|
||||||
|
assert.doesNotThrow(() => new Function(wid), 'виджеты парсятся');
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
assert.ok(/'chemistry-8'.*NULL/s.test(sql) || sql.includes("'chemistry-8', 'chemistry', 8"), 'родитель');
|
||||||
|
for (const ch of CHILDREN) {
|
||||||
|
assert.ok(sql.includes("'" + ch.slug + "'"), 'дитя ' + ch.slug);
|
||||||
|
}
|
||||||
|
// запрет эмоджи (правило проекта)
|
||||||
|
assert.ok(!/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(sql), 'без эмоджи');
|
||||||
|
});
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* Integration tests: /api/lab — curriculum links (Phase 5).
|
||||||
|
* Covers: related (auth), reverse lookup, admin add/delete, validation,
|
||||||
|
* role-gating, textbook/topic existence checks, enabled-filtering of reverse.
|
||||||
|
*/
|
||||||
|
const { describe, it, before, after } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { app, db, inject, getToken, cleanup } = require('./setup');
|
||||||
|
|
||||||
|
// Mount /api/lab on the shared test app.
|
||||||
|
app.use('/api/lab', require('../src/routes/lab'));
|
||||||
|
|
||||||
|
after(() => cleanup());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema-robust insert: fills every NOT NULL column (without a default) that the
|
||||||
|
* caller didn't provide with a safe placeholder, then inserts. Returns lastInsertRowid.
|
||||||
|
* Protects the seed from schema drift (e.g. textbooks.html_path NOT NULL) introduced
|
||||||
|
* by parallel sessions on this branch.
|
||||||
|
*/
|
||||||
|
function seedRow(table, provided) {
|
||||||
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
||||||
|
const colNames = new Set(cols.map(c => c.name));
|
||||||
|
// Keep ONLY keys that are real columns (drops fields absent in this schema —
|
||||||
|
// robust to drift, e.g. topics may lack slug/subject_id on some branches).
|
||||||
|
const row = {};
|
||||||
|
for (const k of Object.keys(provided)) if (colNames.has(k)) row[k] = provided[k];
|
||||||
|
// Fill any required (NOT NULL, no default) column the caller didn't provide.
|
||||||
|
for (const c of cols) {
|
||||||
|
if (c.pk) continue;
|
||||||
|
if (c.name in row) continue;
|
||||||
|
if (c.notnull && c.dflt_value === null) {
|
||||||
|
row[c.name] = /INT|REAL|NUM/i.test(c.type) ? 0 : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const names = Object.keys(row);
|
||||||
|
const ph = names.map(() => '?').join(', ');
|
||||||
|
const info = db.prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${ph})`)
|
||||||
|
.run(...names.map(n => row[n]));
|
||||||
|
return info.lastInsertRowid;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('/api/lab curriculum links', () => {
|
||||||
|
let adminToken, studentToken, tbSlug, topicId;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
adminToken = (await getToken('admin')).token;
|
||||||
|
studentToken = (await getToken('student')).token;
|
||||||
|
// Seed a textbook + topic to link against (schema-robust — fills NOT NULL cols).
|
||||||
|
tbSlug = 'phys-test';
|
||||||
|
seedRow('textbooks', { slug: tbSlug, title: 'Физика тест', subject: 'physics', grade: 9, is_active: 1 });
|
||||||
|
const subjId = seedRow('subjects', { name: 'LinkTest Subj', slug: 'linktest-subj' });
|
||||||
|
topicId = seedRow('topics', { subject_id: subjId, name: 'Колебания тест', slug: 'kolebaniya-test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /related requires auth (401)', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims/pendulum/related', null, null);
|
||||||
|
assert.equal(res.status, 401, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /related returns empty link buckets for a sim with no links', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims/pendulum/related', null, studentToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.sim.id, 'pendulum');
|
||||||
|
assert.deepEqual(res.body.links.textbook, []);
|
||||||
|
assert.deepEqual(res.body.links.topic, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /links is admin-only (student → 403)', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'textbook', ref_id: tbSlug }, studentToken);
|
||||||
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can add a textbook link; label resolved from textbooks', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'textbook', ref_id: tbSlug }, adminToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.link.kind, 'textbook');
|
||||||
|
assert.equal(res.body.link.ref_id, tbSlug);
|
||||||
|
assert.equal(res.body.link.label, 'Физика тест', 'label resolved from textbook title');
|
||||||
|
assert.ok(res.body.link.href.includes(tbSlug), 'href points to textbook');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('related now shows the textbook link', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims/pendulum/related', null, studentToken);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(res.body.links.textbook.length, 1);
|
||||||
|
assert.equal(res.body.links.textbook[0].ref_id, tbSlug);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can add a topic link; label resolved from topics', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'topic', ref_id: String(topicId) }, adminToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.link.label, 'Колебания тест');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('duplicate link → 409', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'textbook', ref_id: tbSlug }, adminToken);
|
||||||
|
assert.equal(res.status, 409, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validation: bad kind → 400', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'nope', ref_id: 'x' }, adminToken);
|
||||||
|
assert.equal(res.status, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validation: missing ref_id → 400', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'textbook' }, adminToken);
|
||||||
|
assert.equal(res.status, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validation: unknown textbook → 404', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'textbook', ref_id: 'ghost-book' }, adminToken);
|
||||||
|
assert.equal(res.status, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validation: unknown topic → 404', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/pendulum/links',
|
||||||
|
{ kind: 'topic', ref_id: '999999' }, adminToken);
|
||||||
|
assert.equal(res.status, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST link to unknown sim → 404', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/ghostsim/links',
|
||||||
|
{ kind: 'textbook', ref_id: tbSlug }, adminToken);
|
||||||
|
assert.equal(res.status, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reverse lookup: GET /links?kind=textbook&ref_id= returns linked enabled sims', async () => {
|
||||||
|
const res = await inject('GET',
|
||||||
|
`/api/lab/links?kind=textbook&ref_id=${encodeURIComponent(tbSlug)}`, null, studentToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.ok(res.body.sims.some(s => s.id === 'pendulum'), 'pendulum in reverse lookup');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reverse lookup excludes disabled sims', async () => {
|
||||||
|
await inject('PATCH', '/api/lab/sims/pendulum', { enabled: false }, adminToken);
|
||||||
|
const res = await inject('GET',
|
||||||
|
`/api/lab/links?kind=textbook&ref_id=${encodeURIComponent(tbSlug)}`, null, studentToken);
|
||||||
|
assert.ok(!res.body.sims.some(s => s.id === 'pendulum'), 'disabled pendulum excluded');
|
||||||
|
await inject('PATCH', '/api/lab/sims/pendulum', { enabled: true }, adminToken); // restore
|
||||||
|
});
|
||||||
|
|
||||||
|
it('batch reverse lookup: GET /links/all?kind=textbook groups by ref_id', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, studentToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.ok(res.body.byRef, 'byRef present');
|
||||||
|
assert.ok(Array.isArray(res.body.byRef[tbSlug]), `byRef[${tbSlug}] is array`);
|
||||||
|
assert.ok(res.body.byRef[tbSlug].some(s => s.id === 'pendulum'), 'pendulum grouped under tbSlug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('batch reverse lookup: bad kind → 400', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/links/all?kind=nope', null, studentToken);
|
||||||
|
assert.equal(res.status, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('batch reverse lookup requires auth (401)', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, null);
|
||||||
|
assert.equal(res.status, 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reverse lookup: bad kind → 400', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/links?kind=nope&ref_id=x', null, studentToken);
|
||||||
|
assert.equal(res.status, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can delete a link; related reflects removal', async () => {
|
||||||
|
// find the textbook link id
|
||||||
|
const rel = await inject('GET', '/api/lab/sims/pendulum/related', null, adminToken);
|
||||||
|
const linkId = rel.body.links.textbook[0].id;
|
||||||
|
const del = await inject('DELETE', `/api/lab/sims/pendulum/links/${linkId}`, null, adminToken);
|
||||||
|
assert.equal(del.status, 200, `got ${del.status}`);
|
||||||
|
const rel2 = await inject('GET', '/api/lab/sims/pendulum/related', null, adminToken);
|
||||||
|
assert.equal(rel2.body.links.textbook.length, 0, 'textbook link gone');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delete unknown link → 404', async () => {
|
||||||
|
const res = await inject('DELETE', '/api/lab/sims/pendulum/links/999999', null, adminToken);
|
||||||
|
assert.equal(res.status, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delete is admin-only (student → 403)', async () => {
|
||||||
|
const res = await inject('DELETE', '/api/lab/sims/pendulum/links/1', null, studentToken);
|
||||||
|
assert.equal(res.status, 403);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* Integration tests: /api/lab/sims — catalog from DB + admin overrides.
|
||||||
|
* Covers: seeded catalog, auth, role-gating, enabled toggle (+legacy mirror),
|
||||||
|
* featured/tags/subject/grade patch, reorder, validation.
|
||||||
|
*/
|
||||||
|
const { describe, it, before, after } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { app, db, inject, getToken, cleanup } = require('./setup');
|
||||||
|
|
||||||
|
// Mount /api/lab on the shared test app (setup builds its own app without it).
|
||||||
|
app.use('/api/lab', require('../src/routes/lab'));
|
||||||
|
|
||||||
|
after(() => cleanup());
|
||||||
|
|
||||||
|
describe('/api/lab/sims', () => {
|
||||||
|
let adminToken, studentToken;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
adminToken = (await getToken('admin')).token;
|
||||||
|
studentToken = (await getToken('student')).token;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/lab/sims requires auth (401 without token)', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims', null, null);
|
||||||
|
assert.equal(res.status, 401, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/lab/sims returns seeded catalog (40 sims) for a student', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims', null, studentToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.module_disabled, false);
|
||||||
|
assert.ok(Array.isArray(res.body.sims), 'sims is array');
|
||||||
|
assert.equal(res.body.sims.length, 40, `expected 40 sims, got ${res.body.sims.length}`);
|
||||||
|
const pend = res.body.sims.find(s => s.id === 'pendulum');
|
||||||
|
assert.ok(pend, 'pendulum present');
|
||||||
|
assert.equal(pend.cat, 'phys');
|
||||||
|
assert.equal(pend.enabled, true);
|
||||||
|
assert.deepEqual(pend.tags, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catalog is ordered by sort_order (graph first, angrybirds last)', async () => {
|
||||||
|
const res = await inject('GET', '/api/lab/sims', null, studentToken);
|
||||||
|
assert.equal(res.body.sims[0].id, 'graph');
|
||||||
|
assert.equal(res.body.sims[res.body.sims.length - 1].id, 'angrybirds');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH /api/lab/sims/:id is admin-only (student → 403)', async () => {
|
||||||
|
const res = await inject('PATCH', '/api/lab/sims/pendulum', { featured: true }, studentToken);
|
||||||
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can disable a sim; it reflects in GET and in legacy sim_disabled_ids', async () => {
|
||||||
|
const res = await inject('PATCH', '/api/lab/sims/waves', { enabled: false }, adminToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.sim.enabled, false);
|
||||||
|
|
||||||
|
const get = await inject('GET', '/api/lab/sims', null, adminToken);
|
||||||
|
const waves = get.body.sims.find(s => s.id === 'waves');
|
||||||
|
assert.equal(waves.enabled, false, 'waves disabled in catalog');
|
||||||
|
|
||||||
|
const legacy = JSON.parse(
|
||||||
|
db.prepare("SELECT value FROM app_settings WHERE key='sim_disabled_ids'").get().value
|
||||||
|
);
|
||||||
|
assert.ok(legacy.includes('waves'), 'waves in legacy sim_disabled_ids');
|
||||||
|
|
||||||
|
await inject('PATCH', '/api/lab/sims/waves', { enabled: true }, adminToken);
|
||||||
|
const legacy2 = JSON.parse(
|
||||||
|
db.prepare("SELECT value FROM app_settings WHERE key='sim_disabled_ids'").get().value
|
||||||
|
);
|
||||||
|
assert.ok(!legacy2.includes('waves'), 'waves removed from legacy after enable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can set featured, tags, subject, grade', async () => {
|
||||||
|
const res = await inject('PATCH', '/api/lab/sims/pendulum',
|
||||||
|
{ featured: true, tags: ['колебания', 'механика'], subject: 'physics', grade: 9 }, adminToken);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(res.body.sim.featured, true);
|
||||||
|
assert.deepEqual(res.body.sim.tags, ['колебания', 'механика']);
|
||||||
|
assert.equal(res.body.sim.subject, 'physics');
|
||||||
|
assert.equal(res.body.sim.grade, 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH rejects bad grade and bad category and non-array tags', async () => {
|
||||||
|
const g = await inject('PATCH', '/api/lab/sims/pendulum', { grade: 99 }, adminToken);
|
||||||
|
assert.equal(g.status, 400, 'bad grade rejected');
|
||||||
|
const c = await inject('PATCH', '/api/lab/sims/pendulum', { cat: 'nope' }, adminToken);
|
||||||
|
assert.equal(c.status, 400, 'bad cat rejected');
|
||||||
|
const t = await inject('PATCH', '/api/lab/sims/pendulum', { tags: 'notarray' }, adminToken);
|
||||||
|
assert.equal(t.status, 400, 'non-array tags rejected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH unknown sim → 404', async () => {
|
||||||
|
const res = await inject('PATCH', '/api/lab/sims/nonexistent', { featured: true }, adminToken);
|
||||||
|
assert.equal(res.status, 404, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /api/lab/sims/reorder updates sort order (admin)', async () => {
|
||||||
|
const get = await inject('GET', '/api/lab/sims', null, adminToken);
|
||||||
|
const ids = get.body.sims.map(s => s.id);
|
||||||
|
const reordered = ['angrybirds', 'graph', ...ids.filter(id => id !== 'angrybirds' && id !== 'graph')];
|
||||||
|
const res = await inject('POST', '/api/lab/sims/reorder', { order: reordered }, adminToken);
|
||||||
|
assert.equal(res.status, 200, `got ${res.status}`);
|
||||||
|
assert.equal(res.body.count, 40);
|
||||||
|
|
||||||
|
const get2 = await inject('GET', '/api/lab/sims', null, adminToken);
|
||||||
|
assert.equal(get2.body.sims[0].id, 'angrybirds', 'angrybirds now first');
|
||||||
|
assert.equal(get2.body.sims[1].id, 'graph', 'graph now second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reorder rejects unknown id and empty order', async () => {
|
||||||
|
const bad = await inject('POST', '/api/lab/sims/reorder', { order: ['ghost'] }, adminToken);
|
||||||
|
assert.equal(bad.status, 400, 'unknown id rejected');
|
||||||
|
const empty = await inject('POST', '/api/lab/sims/reorder', { order: [] }, adminToken);
|
||||||
|
assert.equal(empty.status, 400, 'empty order rejected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reorder is admin-only (student → 403)', async () => {
|
||||||
|
const res = await inject('POST', '/api/lab/sims/reorder', { order: ['graph'] }, studentToken);
|
||||||
|
assert.equal(res.status, 403, `got ${res.status}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/* chem8-textbook.css — фреймворк интерактивных учебников «Химия 8».
|
||||||
|
Палитра amber; структура и классы повторяют учебники физики. */
|
||||||
|
|
||||||
|
:root{
|
||||||
|
--bg:#fffbeb; --card:#fff; --card-soft:#fef9ec; --text:#1c1917; --muted:#78716c; --border:#f0e6cf;
|
||||||
|
--pri:#d97706; --pri-d:#b45309; --pri-l:#fbbf24; --pri-soft:#fef3c7;
|
||||||
|
--sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7;
|
||||||
|
--ok:#15803d; --ok-bg:#dcfce7; --fail:#b91c1c; --fail-bg:#fee2e2; --warn:#b45309; --warn-bg:#fef3c7;
|
||||||
|
--sh:0 1px 3px rgba(120,80,10,.07); --sh2:0 8px 28px rgba(120,80,10,.13);
|
||||||
|
--mono:'JetBrains Mono',ui-monospace,monospace;
|
||||||
|
}
|
||||||
|
html.dark{
|
||||||
|
--bg:#1c1410; --card:#271c14; --card-soft:#2e2118; --text:#fef3c7; --muted:#c9ab82; --border:#4a3520;
|
||||||
|
--pri-soft:rgba(217,119,6,.18); --sec-acc-soft:rgba(217,119,6,.18);
|
||||||
|
--ok-bg:rgba(21,128,61,.2); --fail-bg:rgba(185,28,28,.2); --warn-bg:rgba(180,83,9,.2);
|
||||||
|
}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
html,body{min-height:100vh}
|
||||||
|
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .25s,color .25s}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#92400e 0%,#d97706 55%,#fbbf24 100%);color:#fff;padding:26px 24px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
|
||||||
|
.hdr::before{content:'ХИМИЯ';position:absolute;right:-10px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(3rem,11vw,8rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
|
||||||
|
.hdr-row{position:relative;z-index:1;max-width:1240px;margin:0 auto;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||||||
|
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900;letter-spacing:-.01em}
|
||||||
|
.hdr-sub{font-size:.84rem;opacity:.9;margin-top:3px;max-width:640px}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
|
||||||
|
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit;text-decoration:none}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.26)}
|
||||||
|
|
||||||
|
/* LAYOUT */
|
||||||
|
.main{max-width:1240px;margin:0 auto;padding:22px 24px 60px;display:grid;grid-template-columns:1fr 290px;gap:26px;align-items:start}
|
||||||
|
@media(max-width:980px){.main{grid-template-columns:1fr;padding:16px}}
|
||||||
|
.col-main{min-width:0}
|
||||||
|
.col-side{position:sticky;top:14px;display:flex;flex-direction:column;gap:14px}
|
||||||
|
@media(max-width:980px){.col-side{position:static}}
|
||||||
|
|
||||||
|
/* HERO */
|
||||||
|
.hero{background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.1));border:1px solid var(--border);border-radius:18px;padding:22px 24px;margin-bottom:22px;position:relative;overflow:hidden}
|
||||||
|
.hero h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;color:var(--pri-d);margin-bottom:8px}
|
||||||
|
html.dark .hero h2{color:var(--pri-l)}
|
||||||
|
.hero p{font-size:.92rem;color:var(--text);opacity:.86;max-width:640px;margin-bottom:14px}
|
||||||
|
.hero-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||||||
|
.btn-primary{padding:11px 20px;background:linear-gradient(135deg,var(--pri),var(--pri-l));color:#fff;border:0;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:inherit;box-shadow:var(--sh2)}
|
||||||
|
.btn-primary:hover{filter:brightness(1.07)}
|
||||||
|
.hero-progress{flex:1;min-width:180px}
|
||||||
|
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
||||||
|
.hp-bar{height:8px;background:rgba(217,119,6,.16);border-radius:5px;overflow:hidden;margin:5px 0}
|
||||||
|
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--pri-l));width:0;transition:width .5s}
|
||||||
|
.hp-text{font-size:.8rem;font-weight:700;color:var(--pri-d)}
|
||||||
|
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;background:linear-gradient(135deg,#f59e0b,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif}
|
||||||
|
|
||||||
|
/* PARA-SELECTOR */
|
||||||
|
.psel{margin-bottom:24px}
|
||||||
|
.psel-title{font-family:'Outfit',sans-serif;font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:10px}
|
||||||
|
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}
|
||||||
|
.psel-card{position:relative;background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:13px 14px 16px;cursor:pointer;transition:transform .16s,box-shadow .16s,border-color .16s;overflow:hidden}
|
||||||
|
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
|
||||||
|
.psel-card.active{border-color:var(--pri);box-shadow:0 0 0 2px var(--pri-soft)}
|
||||||
|
.psel-card.final{background:linear-gradient(135deg,var(--pri-soft),var(--card))}
|
||||||
|
.psel-num{font-family:'Outfit';font-weight:800;color:var(--pri);font-size:.84rem;margin-bottom:4px}
|
||||||
|
.psel-name{font-size:.86rem;font-weight:700;line-height:1.3}
|
||||||
|
.psel-sub{font-size:.74rem;color:var(--muted);margin-top:3px}
|
||||||
|
.psel-prog{height:5px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-top:9px}
|
||||||
|
.psel-prog-fill{height:100%;width:0;background:linear-gradient(90deg,var(--pri),var(--pri-l));transition:width .5s}
|
||||||
|
.psel-done{position:absolute;top:9px;right:9px;width:20px;height:20px;border-radius:50%;background:var(--ok);display:none;align-items:center;justify-content:center}
|
||||||
|
.psel-done svg{width:12px;height:12px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.psel-card.done .psel-done{display:flex}
|
||||||
|
|
||||||
|
/* SECTIONS */
|
||||||
|
.sec{display:none}
|
||||||
|
.sec.active{display:block;animation:fadeIn .25s}
|
||||||
|
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
||||||
|
.sec-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
||||||
|
.sec-num{background:linear-gradient(135deg,var(--pri),var(--pri-l));color:#fff;font-family:'Outfit';font-weight:800;font-size:.9rem;padding:6px 13px;border-radius:10px;flex-shrink:0}
|
||||||
|
.sec-h{font-family:'Outfit',sans-serif;font-size:1.25rem;font-weight:800;line-height:1.25}
|
||||||
|
|
||||||
|
/* PARA-HERO */
|
||||||
|
.para-hero{border-radius:16px;padding:20px 22px;color:#fff;position:relative;overflow:hidden;margin-bottom:18px}
|
||||||
|
.para-hero::after{content:'';position:absolute;right:-28px;top:-28px;width:140px;height:140px;border-radius:50%;opacity:.14;background:#fff}
|
||||||
|
.ph-label{font-size:.7rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;opacity:.8;margin-bottom:5px;position:relative;z-index:1}
|
||||||
|
.para-hero h2{font-family:'Outfit',sans-serif;font-size:1.25rem;font-weight:800;margin-bottom:9px;line-height:1.25;position:relative;z-index:1}
|
||||||
|
.ph-formula{display:inline-block;background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.25);border-radius:10px;padding:6px 15px;font-weight:700;margin-bottom:10px;position:relative;z-index:1}
|
||||||
|
.ph-desc{font-size:.88rem;opacity:.92;line-height:1.6;margin-bottom:11px;max-width:680px;position:relative;z-index:1}
|
||||||
|
.ph-tags{display:flex;flex-wrap:wrap;gap:6px;position:relative;z-index:1}
|
||||||
|
.ph-tag{background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.25);border-radius:20px;padding:3px 11px;font-size:.72rem;font-weight:700}
|
||||||
|
.ph-1{background:linear-gradient(135deg,#92400e,#d97706 55%,#fbbf24)}
|
||||||
|
.ph-2{background:linear-gradient(135deg,#134e4a,#0d9488 55%,#2dd4bf)}
|
||||||
|
.ph-3{background:linear-gradient(135deg,#3730a3,#4f46e5 55%,#818cf8)}
|
||||||
|
.ph-4{background:linear-gradient(135deg,#1e3a8a,#2563eb 55%,#60a5fa)}
|
||||||
|
.ph-5{background:linear-gradient(135deg,#064e3b,#059669 55%,#34d399)}
|
||||||
|
.ph-6{background:linear-gradient(135deg,#7c2d12,#ea580c 55%,#fb923c)}
|
||||||
|
.ph-7{background:linear-gradient(135deg,#164e63,#0891b2 55%,#22d3ee)}
|
||||||
|
.ph-8{background:linear-gradient(135deg,#581c87,#9333ea 55%,#c084fc)}
|
||||||
|
.ph-9{background:linear-gradient(135deg,#831843,#db2777 55%,#f472b6)}
|
||||||
|
.ph-pr{background:linear-gradient(135deg,#7c2d12,#c2410c 55%,#fb923c)}
|
||||||
|
.ph-final{background:linear-gradient(135deg,#92400e,#d97706 55%,#f59e0b)}
|
||||||
|
|
||||||
|
/* CARDS */
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:0;box-shadow:var(--sh);margin-bottom:14px;overflow:hidden}
|
||||||
|
.card-header{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border);background:var(--card-soft)}
|
||||||
|
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
|
||||||
|
.card-icon.theory{background:linear-gradient(135deg,#2563eb,#60a5fa)}
|
||||||
|
.card-icon.example{background:linear-gradient(135deg,#059669,#34d399)}
|
||||||
|
.card-icon.rule{background:linear-gradient(135deg,#d97706,#fbbf24)}
|
||||||
|
.card-icon.lab{background:linear-gradient(135deg,#db2777,#f472b6)}
|
||||||
|
.card-icon .ic{width:17px;height:17px;stroke:#fff}
|
||||||
|
.card-title{font-family:'Outfit',sans-serif;font-weight:800;font-size:.96rem;flex:1}
|
||||||
|
.card-num{font-family:'Outfit';font-weight:800;color:var(--muted);font-size:.82rem}
|
||||||
|
.card-body{padding:15px 17px;font-size:.93rem}
|
||||||
|
.card-body p{margin-bottom:9px}.card-body p:last-child{margin-bottom:0}
|
||||||
|
.card-body ul,.card-body ol{margin:6px 0 9px 20px}
|
||||||
|
.card-body li{margin-bottom:4px}
|
||||||
|
.card-body b{color:var(--pri-d)}
|
||||||
|
html.dark .card-body b{color:var(--pri-l)}
|
||||||
|
|
||||||
|
.section-title{font-family:'Outfit';font-weight:800;font-size:1rem;margin:14px 0 10px;color:var(--pri-d)}
|
||||||
|
html.dark .section-title{color:var(--pri-l)}
|
||||||
|
.formula-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin:10px 0}
|
||||||
|
.fcard{background:var(--card-soft);border:1.5px solid var(--border);border-radius:12px;padding:13px 15px}
|
||||||
|
.fcard.highlight{border-color:var(--pri);background:var(--pri-soft)}
|
||||||
|
.fcard h3{font-family:'Outfit';font-size:.9rem;font-weight:800;margin-bottom:6px}
|
||||||
|
.main-f{font-size:1.05rem;font-weight:700;color:var(--pri-d);font-family:var(--mono)}
|
||||||
|
html.dark .main-f{color:var(--pri-l)}
|
||||||
|
|
||||||
|
.def-box{background:var(--pri-soft);border-left:4px solid var(--pri);border-radius:0 10px 10px 0;padding:12px 16px;margin:10px 0;font-size:.91rem;line-height:1.7}
|
||||||
|
.def-box b{color:var(--pri-d)}html.dark .def-box b{color:var(--pri-l)}
|
||||||
|
.remember-box{background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border:1.5px solid var(--pri-l);border-radius:13px;padding:14px 17px;margin:14px 0}
|
||||||
|
.remember-box-title{font-weight:800;font-size:.86rem;color:#92400e;margin-bottom:8px;display:flex;align-items:center;gap:7px}
|
||||||
|
html.dark .remember-box-title{color:#fde68a}
|
||||||
|
.remember-box ul{margin:0 0 0 18px;font-size:.88rem}
|
||||||
|
.remember-box li{margin-bottom:5px}
|
||||||
|
.insight-box{background:linear-gradient(135deg,rgba(79,70,229,.07),rgba(139,92,246,.04));border:2px solid rgba(79,70,229,.2);border-radius:13px;padding:13px 16px;margin:14px 0}
|
||||||
|
.insight-title{font-weight:800;font-size:.82rem;color:#4f46e5;margin-bottom:7px;display:flex;align-items:center;gap:7px}
|
||||||
|
html.dark .insight-title{color:#a5b4fc}
|
||||||
|
.insight-box p{font-size:.85rem;line-height:1.75;margin-bottom:5px}
|
||||||
|
.note-safe{display:flex;gap:9px;background:var(--warn-bg);border:1px solid var(--pri-l);border-radius:10px;padding:10px 13px;font-size:.86rem;margin:10px 0}
|
||||||
|
.note-safe svg{stroke:var(--pri-d);margin-top:2px;width:18px;height:18px;fill:none;stroke-width:2}
|
||||||
|
|
||||||
|
/* life-grid */
|
||||||
|
.life-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:14px 0}
|
||||||
|
.life-item{background:var(--card);border:1.5px solid var(--border);border-radius:12px;padding:13px 11px;text-align:center}
|
||||||
|
.li-icon{display:flex;justify-content:center;margin-bottom:7px}
|
||||||
|
.li-icon svg{width:26px;height:26px;stroke:var(--pri);fill:none;stroke-width:1.8}
|
||||||
|
.li-title{font-size:.82rem;font-weight:800;margin-bottom:3px}
|
||||||
|
.li-desc{font-size:.74rem;color:var(--muted);line-height:1.5}
|
||||||
|
|
||||||
|
/* q-list */
|
||||||
|
.q-list{margin:8px 0 0 20px;font-size:.9rem}
|
||||||
|
.q-list li{margin-bottom:7px;line-height:1.6}
|
||||||
|
|
||||||
|
/* TASKS */
|
||||||
|
.legacy-tasks{margin-top:20px;padding:16px 18px;background:var(--card);border:1.5px solid var(--border);border-radius:14px}
|
||||||
|
.lt-head{display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
|
||||||
|
.lt-title{font-weight:800;font-family:'Outfit'}
|
||||||
|
.chip{padding:3px 11px;border-radius:99px;font-weight:700;font-size:.8rem}
|
||||||
|
.chip-ok{margin-left:auto;background:var(--ok-bg);color:var(--ok)}
|
||||||
|
.chip-tot{background:rgba(120,80,10,.08);color:var(--muted)}
|
||||||
|
.lt-reset{padding:5px 11px;font-size:.78rem}
|
||||||
|
.prog-wrap{height:5px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-bottom:10px}
|
||||||
|
.prog-fill{height:100%;width:0;background:linear-gradient(90deg,var(--pri),var(--pri-l));transition:width .4s}
|
||||||
|
.nav-dots{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}
|
||||||
|
.nav-dot{min-width:30px;height:30px;padding:0 6px;border-radius:8px;border:2px solid var(--border);background:var(--card);font-size:.74rem;font-weight:700;cursor:pointer;display:grid;place-items:center;color:var(--muted);font-family:var(--mono);transition:.15s}
|
||||||
|
.nav-dot:hover{border-color:var(--pri);color:var(--pri)}
|
||||||
|
.nav-dot.nd-cur{background:var(--pri);border-color:var(--pri);color:#fff}
|
||||||
|
.nav-dot.nd-ok{background:var(--ok-bg);border-color:var(--ok);color:var(--ok)}
|
||||||
|
.nav-dot.nd-fail{background:var(--fail-bg);border-color:var(--fail);color:var(--fail)}
|
||||||
|
.task-card{background:var(--card-soft);border:1px solid var(--border);border-radius:12px;padding:14px 16px}
|
||||||
|
.task-num{font-size:.74rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
|
||||||
|
.task-text{font-size:.94rem;line-height:1.65;margin-bottom:11px}
|
||||||
|
.task-hint{display:flex;gap:7px;align-items:flex-start;background:var(--warn-bg);border-radius:9px;padding:8px 12px;font-size:.84rem;margin-bottom:11px;color:var(--text)}
|
||||||
|
.task-hint svg{stroke:var(--pri-d);width:15px;height:15px;flex-shrink:0;margin-top:2px}
|
||||||
|
.ans-row{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
|
||||||
|
.ans-row label{font-weight:700;font-size:.88rem}
|
||||||
|
.ans-inp{padding:8px 12px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-family:var(--mono);width:120px;font-size:.95rem}
|
||||||
|
.ans-inp:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.unit-lbl{font-size:.86rem;color:var(--muted);font-weight:600}
|
||||||
|
.mcq-opts{display:flex;flex-direction:column;gap:8px}
|
||||||
|
.mcq-opt{width:100%;text-align:left;padding:11px 15px;border:2px solid var(--border);border-radius:10px;background:var(--card);color:var(--text);font-size:.9rem;cursor:pointer;transition:.16s;line-height:1.5;font-family:inherit}
|
||||||
|
.mcq-opt:hover:not(:disabled){border-color:var(--pri);background:var(--pri-soft)}
|
||||||
|
.mcq-let{font-weight:800;margin-right:6px;color:var(--pri)}
|
||||||
|
.mcq-opt.mcq-cor{border-color:var(--ok)!important;background:var(--ok-bg)!important;color:var(--ok)!important;font-weight:700}
|
||||||
|
.mcq-opt.mcq-wrong{border-color:var(--fail)!important;background:var(--fail-bg)!important;color:var(--fail)!important}
|
||||||
|
.feedback{display:none;padding:11px 14px;border-radius:10px;font-size:.89rem;margin-top:10px;line-height:1.55}
|
||||||
|
.feedback.show{display:block}
|
||||||
|
.feedback.fb-ok{background:var(--ok-bg);color:var(--ok);border-left:4px solid var(--ok)}
|
||||||
|
.feedback.fb-fail{background:var(--fail-bg);color:var(--fail);border-left:4px solid var(--fail)}
|
||||||
|
.feedback b{font-weight:800}
|
||||||
|
.lt-foot{display:flex;justify-content:flex-end;margin-top:10px}
|
||||||
|
.summary{display:none;text-align:center;padding:16px;margin-top:12px;background:linear-gradient(135deg,var(--pri-soft),var(--card));border-radius:12px}
|
||||||
|
.summary.show{display:block}
|
||||||
|
.sum-t{font-weight:800;margin-bottom:5px;font-family:'Outfit'}
|
||||||
|
.big-score{font-size:1.6rem;font-weight:900;color:var(--pri-d)}
|
||||||
|
html.dark .big-score{color:var(--pri-l)}
|
||||||
|
.sum-grade{margin-top:5px;color:var(--muted);font-size:.88rem}
|
||||||
|
|
||||||
|
/* BUTTONS */
|
||||||
|
.btn{font-family:inherit;font-weight:700;font-size:.88rem;padding:8px 15px;border-radius:9px;border:1.5px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;transition:.15s;display:inline-flex;align-items:center;gap:7px}
|
||||||
|
.btn:hover{border-color:var(--pri);background:var(--pri-soft)}
|
||||||
|
.btn.primary{background:linear-gradient(135deg,var(--pri),var(--pri-l));color:#fff;border-color:transparent}
|
||||||
|
.btn.primary:hover{filter:brightness(1.08)}
|
||||||
|
.sec-nav{display:flex;justify-content:space-between;gap:12px;margin-top:20px}
|
||||||
|
.read-wrap{margin-top:18px;display:flex;justify-content:center}
|
||||||
|
|
||||||
|
/* SIDEBAR cards */
|
||||||
|
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:13px;padding:14px 16px;box-shadow:var(--sh)}
|
||||||
|
.sidecard h4{font-family:'Outfit';font-size:.86rem;font-weight:800;margin-bottom:9px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sidecard h4 svg{width:14px;height:14px}
|
||||||
|
.sidecard-row{font-size:.85rem;padding:5px 0;border-bottom:1px dashed var(--border);line-height:1.5}
|
||||||
|
.sidecard-row:last-child{border-bottom:0}
|
||||||
|
.sidecard-row b{color:var(--pri-d);font-weight:700}
|
||||||
|
html.dark .sidecard-row b{color:var(--pri-l)}
|
||||||
|
.sidecard-row.done{color:var(--ok);border-bottom:0;padding:3px 0}
|
||||||
|
.sidecard.tip{background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--pri-l)}
|
||||||
|
.sidecard.tip h4{color:#92400e}html.dark .sidecard.tip h4{color:#fde68a}
|
||||||
|
.xp-card{background:linear-gradient(135deg,var(--pri),var(--pri-d));color:#fff;border-radius:13px;padding:14px 16px;box-shadow:var(--sh)}
|
||||||
|
.xp-card-title{display:flex;justify-content:space-between;font-size:.78rem;font-weight:700;margin-bottom:8px}
|
||||||
|
.xp-level{background:rgba(255,255,255,.22);padding:2px 9px;border-radius:99px;font-weight:800}
|
||||||
|
.xp-bar{height:7px;background:rgba(255,255,255,.25);border-radius:4px;overflow:hidden}
|
||||||
|
.xp-fill{height:100%;background:#fff;transition:width .5s}
|
||||||
|
.xp-nums{display:flex;justify-content:space-between;font-size:.72rem;margin-top:5px;opacity:.9}
|
||||||
|
|
||||||
|
/* FLAGSHIP */
|
||||||
|
.flag-card{position:relative;background:linear-gradient(135deg,var(--card),var(--pri-soft));border:2px solid var(--pri);border-radius:16px;padding:18px 20px;margin:16px 0}
|
||||||
|
.flag-card::before{content:'★ ФЛАГМАН';position:absolute;top:12px;right:14px;background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#fff;padding:4px 11px;border-radius:99px;font-weight:800;font-size:.66rem;letter-spacing:.03em}
|
||||||
|
.flag-title{font-family:'Outfit';font-weight:800;font-size:1.02rem;color:var(--pri-d);margin-bottom:4px;padding-right:90px}
|
||||||
|
html.dark .flag-title{color:var(--pri-l)}
|
||||||
|
.flag-help{font-size:.84rem;color:var(--muted);margin-bottom:12px}
|
||||||
|
|
||||||
|
/* WIDGET shell (общий для виджетов §) */
|
||||||
|
.wgt{background:var(--card);border:1.5px solid var(--pri-soft);border-radius:14px;padding:16px 18px;box-shadow:var(--sh);margin:14px 0}
|
||||||
|
.wgt-h{font-family:'Outfit';font-size:.94rem;font-weight:800;color:var(--pri-d);margin-bottom:10px;display:flex;align-items:center;gap:8px}
|
||||||
|
html.dark .wgt-h{color:var(--pri-l)}
|
||||||
|
.wgt-h svg{stroke:var(--pri);width:18px;height:18px;fill:none;stroke-width:2}
|
||||||
|
.fld{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0}
|
||||||
|
.fld label{font-size:.85rem;font-weight:600;color:var(--muted)}
|
||||||
|
.wgt input[type=text],.wgt input[type=number],.wgt select{font-family:inherit;font-size:.94rem;padding:8px 11px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text)}
|
||||||
|
.wgt input:focus,.wgt select:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.out{margin-top:10px;padding:11px 14px;border-radius:10px;font-size:.92rem;background:var(--card-soft);border:1px solid var(--border)}
|
||||||
|
.out.ok{background:var(--ok-bg);border-color:#86efac;color:var(--ok)}
|
||||||
|
.out.bad{background:var(--fail-bg);border-color:#fca5a5;color:var(--fail)}
|
||||||
|
.bd{font-family:var(--mono);font-size:.88rem;line-height:1.75}
|
||||||
|
|
||||||
|
/* mole triangle */
|
||||||
|
.mtri{display:grid;grid-template-columns:170px 1fr;gap:16px;align-items:center}
|
||||||
|
@media(max-width:560px){.mtri{grid-template-columns:1fr}}
|
||||||
|
.mtri-svg{width:170px;height:128px;color:var(--pri)}
|
||||||
|
.mtri-fields{display:flex;flex-direction:column;gap:9px}
|
||||||
|
.mtri-f{display:flex;flex-direction:column;gap:3px}
|
||||||
|
.mtri-lab{font-size:.78rem;font-weight:700;color:var(--muted)}
|
||||||
|
.mtri-f input{width:100%;padding:8px 11px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-family:var(--mono)}
|
||||||
|
.mtri-f input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.mtri-out{grid-column:1/-1;padding:10px 13px;border-radius:10px;background:var(--card-soft);border:1px solid var(--border);font-size:.9rem}
|
||||||
|
.mtri-out.ok{background:var(--ok-bg);border-color:#86efac;color:var(--ok)}
|
||||||
|
.mtri-out b{display:block;font-size:1.02rem}
|
||||||
|
.mtri-form{display:block;font-family:var(--mono);font-size:.83rem;opacity:.85;margin-top:3px}
|
||||||
|
|
||||||
|
/* equation balancer */
|
||||||
|
.ceqb-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:1.05rem;font-weight:600;margin-bottom:12px}
|
||||||
|
.ceqb-sp{display:inline-flex;align-items:center;gap:3px}
|
||||||
|
.ceqb-coef{width:46px;text-align:center;padding:6px 4px;font-weight:800;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:var(--mono)}
|
||||||
|
.ceqb-coef:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.ceqb-f{font-weight:700}
|
||||||
|
.ceqb-plus,.ceqb-arrow{color:var(--muted);font-weight:800;padding:0 2px}
|
||||||
|
.ceqb-arrow{color:var(--pri);font-size:1.2rem}
|
||||||
|
.ceqb-actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||||
|
.ceqb-out{margin-top:10px}
|
||||||
|
.ceqb-msg{font-weight:700;margin-bottom:6px}
|
||||||
|
.ceqb-out.ok .ceqb-msg{color:var(--ok)}
|
||||||
|
.ceqb-out.bad .ceqb-msg{color:var(--fail)}
|
||||||
|
.ceqb-tab{border-collapse:collapse;font-size:.84rem;font-family:var(--mono)}
|
||||||
|
.ceqb-tab th,.ceqb-tab td{border:1px solid var(--border);padding:4px 12px;text-align:center}
|
||||||
|
.ceqb-tab tr.ne td{background:var(--fail-bg);color:var(--fail)}
|
||||||
|
.ceqb-tab tr.eq td{background:var(--ok-bg);color:var(--ok)}
|
||||||
|
.ceqb-btn{font-family:inherit;font-weight:700;font-size:.86rem;padding:7px 14px;border-radius:9px;border:1.5px solid var(--border);background:var(--card);color:var(--text);cursor:pointer}
|
||||||
|
.ceqb-btn.primary{background:linear-gradient(135deg,var(--pri),var(--pri-l));color:#fff;border-color:transparent}
|
||||||
|
|
||||||
|
/* element grid */
|
||||||
|
.el-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:6px;margin-top:8px}
|
||||||
|
.el-cell{aspect-ratio:1;border:1px solid var(--border);border-radius:8px;background:var(--card);display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:.12s;padding:2px}
|
||||||
|
.el-cell:hover,.el-cell.on{background:var(--pri-soft);border-color:var(--pri);transform:translateY(-2px)}
|
||||||
|
.el-cell .z{font-size:.58rem;color:var(--muted)}
|
||||||
|
.el-cell .s{font-size:1.02rem;font-weight:800;color:var(--pri-d)}
|
||||||
|
html.dark .el-cell .s{color:var(--pri-l)}
|
||||||
|
.el-cell .a{font-size:.54rem;color:var(--muted)}
|
||||||
|
.el-info{margin-top:10px;padding:12px 14px;border-radius:10px;background:var(--card-soft);border:1px solid var(--border);font-size:.92rem;min-height:46px}
|
||||||
|
|
||||||
|
/* DnD */
|
||||||
|
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:48px;margin-bottom:10px}
|
||||||
|
.dnd-chip{padding:7px 13px;border:1.5px solid var(--border);border-radius:10px;cursor:grab;background:var(--card);font-size:.86rem;font-weight:600;user-select:none}
|
||||||
|
.dnd-chip.placed{background:var(--pri-soft);border-color:var(--pri)}
|
||||||
|
.dnd-zones{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
|
||||||
|
.drop-box{border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:80px;background:var(--card-soft)}
|
||||||
|
.drop-box.over{border-color:var(--pri);background:var(--pri-soft);border-style:solid}
|
||||||
|
.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}
|
||||||
|
|
||||||
|
/* miniPeriodic */
|
||||||
|
.pt-wrap{overflow-x:auto;padding-bottom:6px}
|
||||||
|
.pt-grid{display:grid;grid-template-columns:repeat(18,minmax(30px,1fr));grid-auto-rows:34px;gap:2px;min-width:600px}
|
||||||
|
.pt-cell{position:relative;border:1px solid var(--border);border-radius:5px;background:var(--card);cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1px;transition:.1s;overflow:hidden}
|
||||||
|
.pt-cell:hover{transform:scale(1.12);z-index:2;border-color:var(--pri)}
|
||||||
|
.pt-z{font-size:.5rem;color:var(--muted);line-height:1}
|
||||||
|
.pt-s{font-size:.74rem;font-weight:800;line-height:1.05}
|
||||||
|
.pt-metal{background:rgba(13,148,136,.12)}
|
||||||
|
.pt-nonmetal{background:rgba(245,158,11,.16)}
|
||||||
|
.pt-metalloid{background:rgba(124,58,237,.13)}
|
||||||
|
.pt-noble{background:rgba(37,99,235,.13)}
|
||||||
|
.pt-lanth,.pt-act{background:rgba(219,39,119,.12)}
|
||||||
|
.pt-lanth .pt-z,.pt-act .pt-z{font-size:.44rem}
|
||||||
|
.pt-cell.pt-hot{outline:2.5px solid var(--pri);outline-offset:-2px;z-index:1;box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.pt-cell.pt-sel{background:var(--pri);border-color:var(--pri)}
|
||||||
|
.pt-cell.pt-sel .pt-s,.pt-cell.pt-sel .pt-z{color:#fff}
|
||||||
|
.pt-info{margin-top:10px;padding:11px 14px;border-radius:10px;background:var(--card-soft);border:1px solid var(--border);font-size:.92rem}
|
||||||
|
.pt-info b{color:var(--pri-d)}html.dark .pt-info b{color:var(--pri-l)}
|
||||||
|
.pt-legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:.76rem;color:var(--muted)}
|
||||||
|
.pt-legend span{display:inline-flex;align-items:center;gap:5px}
|
||||||
|
.pt-legend i{width:12px;height:12px;border-radius:3px;display:inline-block}
|
||||||
|
.pt-modes{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
|
||||||
|
|
||||||
|
/* модель атома (§29,33) */
|
||||||
|
.as-svg{width:100%;max-width:320px;height:auto;color:var(--pri);display:block;margin:8px auto}
|
||||||
|
.as-stage{display:flex;justify-content:center}
|
||||||
|
.as-cfg{margin-top:6px}
|
||||||
|
.as-zl{font-weight:800;color:var(--pri-d)}html.dark .as-zl{color:var(--pri-l)}
|
||||||
|
|
||||||
|
/* паспорт элемента (§35) */
|
||||||
|
.passport{margin-top:10px;padding:13px 16px;border-radius:11px;background:var(--card-soft);border:1px solid var(--border)}
|
||||||
|
.passport h4{font-family:'Outfit';font-weight:800;margin-bottom:8px;color:var(--pri-d)}
|
||||||
|
html.dark .passport h4{color:var(--pri-l)}
|
||||||
|
.passport-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;font-size:.85rem}
|
||||||
|
.passport-grid div{padding:6px 9px;background:var(--card);border:1px solid var(--border);border-radius:8px}
|
||||||
|
.passport-grid b{color:var(--pri-d)}html.dark .passport-grid b{color:var(--pri-l)}
|
||||||
|
|
||||||
|
/* тип связи (§37,38) */
|
||||||
|
.bt-svg{width:100%;max-width:280px;height:auto;color:var(--text);display:block;margin:8px auto}
|
||||||
|
.bt-stage{display:flex;justify-content:center}
|
||||||
|
.bt-out.ok{background:var(--ok-bg);border-color:#86efac}
|
||||||
|
.bt-out.bad{background:var(--fail-bg);border-color:#fca5a5}
|
||||||
|
|
||||||
|
/* решётки (§41) */
|
||||||
|
.lat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:10px 0}
|
||||||
|
.lat-card{border:1.5px solid var(--border);border-radius:12px;padding:13px 15px;background:var(--card-soft)}
|
||||||
|
.lat-card h4{font-family:'Outfit';font-weight:800;font-size:.92rem;margin-bottom:6px;color:var(--pri-d)}
|
||||||
|
html.dark .lat-card h4{color:var(--pri-l)}
|
||||||
|
.lat-card .lat-ex{font-family:var(--mono);font-size:.82rem;color:var(--muted);margin-bottom:4px}
|
||||||
|
.lat-card ul{margin:4px 0 0 16px;font-size:.82rem}
|
||||||
|
|
||||||
|
/* орбитали (§32) — статичные SVG */
|
||||||
|
.orb-row{display:flex;gap:18px;flex-wrap:wrap;justify-content:center;margin:10px 0}
|
||||||
|
.orb-item{text-align:center}
|
||||||
|
.orb-item svg{width:90px;height:90px;color:var(--pri)}
|
||||||
|
.orb-item .orb-lab{font-size:.82rem;font-weight:700;margin-top:4px}
|
||||||
|
|
||||||
|
/* амфотерность (§25) */
|
||||||
|
.amph-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||||||
|
.amph-stage{display:flex;justify-content:center;margin:8px 0}
|
||||||
|
.amph-out{margin-top:6px}
|
||||||
|
|
||||||
|
/* 3D-модели молекул/решёток (§38,41) */
|
||||||
|
.mol-cv{background:#0b1220;cursor:grab;border:1px solid var(--border)}
|
||||||
|
.mol-cv:active{cursor:grabbing}
|
||||||
|
.mol-info{margin-top:8px}
|
||||||
|
|
||||||
|
/* геном-карта классов (§22) */
|
||||||
|
.gm-svg{width:100%;max-width:440px;height:auto;color:var(--text);display:block;margin:4px auto}
|
||||||
|
.gm-out{margin-top:8px}
|
||||||
|
.gm-edge{transition:stroke .15s,stroke-width .15s}
|
||||||
|
|
||||||
|
/* диссоциация/растворение (§47) */
|
||||||
|
.ds-svg{width:100%;max-width:300px;height:auto;display:block;margin:6px auto}
|
||||||
|
.ds-stage{display:flex;justify-content:center}
|
||||||
|
.ds-out{margin-top:6px}
|
||||||
|
|
||||||
|
/* 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}
|
||||||
|
.ach-popup svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2}
|
||||||
|
.ach-popup.show{transform:translateX(-50%) translateY(0)}
|
||||||
|
.ach-popup.gold{background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#fff;border-color:transparent}
|
||||||
|
.ach-popup.gold svg{stroke:#fff}
|
||||||
@@ -1,90 +1,78 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
/* admin → sims (simulations) section */
|
/* admin → sims (simulations) section — контент-движок, Фазы 4-5.
|
||||||
|
*
|
||||||
|
* Каталог берётся из БД (/api/lab/sims), а НЕ из захардкоженного списка.
|
||||||
|
* Управление: вкл/выкл (зеркалится в legacy sim_disabled_ids), «рекомендуемая»,
|
||||||
|
* курикулумные связи (Фаза 5). Мастер-тумблер модуля — /api/settings/sims. */
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
let inited = false;
|
let inited = false;
|
||||||
|
|
||||||
// Full list of available (non-null id) sims mirrored from /lab
|
const CAT_LABEL = { math: 'Математика', phys: 'Физика', chem: 'Химия', bio: 'Биология', game: 'Игры' };
|
||||||
const ADMIN_SIMS = [
|
const CAT_ORDER = ['math', 'phys', 'chem', 'bio', 'game'];
|
||||||
{ id: 'graph', cat: 'Математика', title: 'График функции' },
|
|
||||||
{ id: 'graphtransform', cat: 'Математика', title: 'Трансформации графиков' },
|
|
||||||
{ id: 'geometry', cat: 'Математика', title: 'Планиметрия' },
|
|
||||||
{ id: 'triangle', cat: 'Математика', title: 'Геометрия треугольника' },
|
|
||||||
{ id: 'quadratic', cat: 'Математика', title: 'Корни квадратного уравнения' },
|
|
||||||
{ id: 'stereo', cat: 'Математика', title: 'Стереометрия 3D' },
|
|
||||||
{ id: 'probability', cat: 'Математика', title: 'Теория вероятностей' },
|
|
||||||
{ id: 'trigcircle', cat: 'Математика', title: 'Тригонометрическая окружность' },
|
|
||||||
{ id: 'normaldist', cat: 'Математика', title: 'Нормальное распределение' },
|
|
||||||
{ id: 'projectile', cat: 'Физика', title: 'Бросок тела' },
|
|
||||||
{ id: 'pendulum', cat: 'Физика', title: 'Маятник' },
|
|
||||||
{ id: 'collision', cat: 'Физика', title: 'Столкновение шаров' },
|
|
||||||
{ id: 'emfield', cat: 'Физика', title: 'Электромагнитные поля' },
|
|
||||||
{ id: 'circuit', cat: 'Физика', title: 'Электрические цепи' },
|
|
||||||
{ id: 'hydrostatics', cat: 'Физика', title: 'Гидростатика' },
|
|
||||||
{ id: 'dynamics', cat: 'Физика', title: 'Динамика' },
|
|
||||||
{ id: 'opticsbench', cat: 'Физика', title: 'Оптическая скамья' },
|
|
||||||
{ id: 'isoprocess', cat: 'Физика', title: 'Изопроцессы' },
|
|
||||||
{ id: 'waves', cat: 'Физика', title: 'Волны и звук' },
|
|
||||||
{ id: 'heatengine', cat: 'Физика', title: 'Тепловые двигатели' },
|
|
||||||
{ id: 'radioactive', cat: 'Физика', title: 'Радиоактивный распад' },
|
|
||||||
{ id: 'race', cat: 'Физика', title: 'Гонка с задачами' },
|
|
||||||
{ id: 'logic', cat: 'Физика', title: 'Логические схемы' },
|
|
||||||
{ id: 'molphys', cat: 'Химия', title: 'Молекулярная физика' },
|
|
||||||
{ id: 'chemistry', cat: 'Химия', title: 'Химические реакции' },
|
|
||||||
{ id: 'equilibrium', cat: 'Химия', title: 'Химическое равновесие' },
|
|
||||||
{ id: 'electrolysis', cat: 'Химия', title: 'Электролиз' },
|
|
||||||
{ id: 'bohratom', cat: 'Химия', title: 'Атом Бора' },
|
|
||||||
{ id: 'orbitals', cat: 'Химия', title: 'Молекулярные орбитали' },
|
|
||||||
{ id: 'titration', cat: 'Химия', title: 'pH и кривая титрования' },
|
|
||||||
{ id: 'chemsandbox', cat: 'Химия', title: 'Химическая песочница' },
|
|
||||||
{ id: 'stoichiometry', cat: 'Химия', title: 'Стехиометрия' },
|
|
||||||
{ id: 'crystal', cat: 'Химия', title: 'Кристаллическая решётка' },
|
|
||||||
{ id: 'qualanalysis', cat: 'Химия', title: 'Качественный анализ' },
|
|
||||||
{ id: 'periodic', cat: 'Химия', title: 'Периодическая таблица' },
|
|
||||||
{ id: 'organic', cat: 'Химия', title: 'Органическая химия' },
|
|
||||||
{ id: 'solutions', cat: 'Химия', title: 'Растворы' },
|
|
||||||
{ id: 'celldivision', cat: 'Биология', title: 'Деление клетки' },
|
|
||||||
{ id: 'photosynthesis', cat: 'Биология', title: 'Фотосинтез и дыхание' },
|
|
||||||
{ id: 'angrybirds', cat: 'Игры', title: 'Angry Birds Physics' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let _simsSettings = { module_disabled: false, disabled_ids: [] };
|
let _moduleDisabled = false;
|
||||||
|
let _sims = []; // [{id,cat,title,enabled,featured,tags,subject,grade,sort}]
|
||||||
|
let _textbooks = null; // кэш каталога учебников для выпадающего списка связей
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/[&<>"']/g, c =>
|
||||||
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const data = await LS.api('/api/settings/sims');
|
const data = await LS.api('/api/lab/sims');
|
||||||
_simsSettings = data;
|
_moduleDisabled = !!data.module_disabled;
|
||||||
|
_sims = Array.isArray(data.sims) ? data.sims : [];
|
||||||
_render();
|
_render();
|
||||||
} catch(e) { LS.toast('Ошибка загрузки настроек: ' + e.message, 'error'); }
|
} catch (e) { LS.toast('Ошибка загрузки симуляций: ' + e.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function _render() {
|
function _render() {
|
||||||
// master toggle
|
|
||||||
const masterChk = document.getElementById('sims-master-chk');
|
const masterChk = document.getElementById('sims-master-chk');
|
||||||
if (masterChk) masterChk.checked = !_simsSettings.module_disabled;
|
if (masterChk) masterChk.checked = !_moduleDisabled;
|
||||||
|
|
||||||
// per-sim cards
|
|
||||||
const grid = document.getElementById('sims-grid');
|
const grid = document.getElementById('sims-grid');
|
||||||
const dis = new Set(_simsSettings.disabled_ids || []);
|
if (!grid) return;
|
||||||
// group by category
|
|
||||||
|
// group by category, preserving catalogue sort within group
|
||||||
const byCat = {};
|
const byCat = {};
|
||||||
ADMIN_SIMS.forEach(s => { (byCat[s.cat] = byCat[s.cat] || []).push(s); });
|
_sims.forEach(s => { (byCat[s.cat] = byCat[s.cat] || []).push(s); });
|
||||||
|
const cats = CAT_ORDER.filter(c => byCat[c]).concat(
|
||||||
|
Object.keys(byCat).filter(c => !CAT_ORDER.includes(c)));
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
Object.entries(byCat).forEach(([cat, sims]) => {
|
cats.forEach(cat => {
|
||||||
html += `<div style="grid-column:1/-1;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--text-3);margin-top:12px;margin-bottom:2px">${esc(cat)}</div>`;
|
html += `<div style="grid-column:1/-1;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--text-3);margin-top:12px;margin-bottom:2px">${esc(CAT_LABEL[cat] || cat)}</div>`;
|
||||||
sims.forEach(s => {
|
byCat[cat].forEach(s => {
|
||||||
const enabled = !dis.has(s.id);
|
const tags = (s.tags || []).map(t => esc(t)).join(', ');
|
||||||
html += `<div class="perm-card${enabled ? ' enabled' : ''}" id="simcard-${s.id}">
|
html += `<div class="perm-card${s.enabled ? ' enabled' : ''}" id="simcard-${esc(s.id)}" style="flex-wrap:wrap">
|
||||||
<div class="perm-info">
|
<div class="perm-info">
|
||||||
<div class="perm-label">${esc(s.title)}</div>
|
<div class="perm-label">
|
||||||
<div class="perm-desc" style="font-size:11px;margin-top:2px;opacity:.7">${esc(s.id)}</div>
|
${esc(s.title)}
|
||||||
|
<button class="sim-star" title="${s.featured ? 'Убрать из рекомендуемых' : 'Сделать рекомендуемой'}"
|
||||||
|
onclick="simToggleFeatured('${esc(s.id)}', ${s.featured ? 'false' : 'true'})"
|
||||||
|
style="background:none;border:none;cursor:pointer;padding:0 0 0 6px;vertical-align:middle">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;${s.featured ? 'fill:var(--amber);stroke:var(--amber)' : 'fill:none;stroke:var(--text-3)'}"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="perm-desc" style="font-size:11px;margin-top:2px;opacity:.7">${esc(s.id)}${tags ? ' · ' + tags : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="perm-toggle" title="${enabled ? 'Отключить' : 'Включить'}">
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="simToggleOne('${s.id}', this.checked)" />
|
<button class="sim-links-btn" title="Связи с программой"
|
||||||
<span class="perm-track"></span>
|
onclick="simToggleLinks('${esc(s.id)}')"
|
||||||
<span class="perm-thumb"></span>
|
style="background:none;border:1px solid var(--border,rgba(255,255,255,.14));border-radius:8px;cursor:pointer;padding:4px 8px;font-size:.7rem;color:var(--text-2);display:inline-flex;align-items:center;gap:4px">
|
||||||
</label>
|
<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||||
|
Связи
|
||||||
|
</button>
|
||||||
|
<label class="perm-toggle" title="${s.enabled ? 'Отключить' : 'Включить'}">
|
||||||
|
<input type="checkbox" ${s.enabled ? 'checked' : ''} onchange="simToggleOne('${esc(s.id)}', this.checked)" />
|
||||||
|
<span class="perm-track"></span>
|
||||||
|
<span class="perm-thumb"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="sim-links-panel" id="simlinks-${esc(s.id)}" style="display:none;flex-basis:100%;width:100%;margin-top:8px;padding-top:8px;border-top:1px solid var(--border,rgba(255,255,255,.1))"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -95,26 +83,126 @@
|
|||||||
async function simsMasterToggle(checked) {
|
async function simsMasterToggle(checked) {
|
||||||
try {
|
try {
|
||||||
await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ module_disabled: !checked }) });
|
await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ module_disabled: !checked }) });
|
||||||
_simsSettings.module_disabled = !checked;
|
_moduleDisabled = !checked;
|
||||||
LS.toast(checked ? 'Модуль симуляций включён' : 'Модуль симуляций отключён', checked ? 'success' : 'warning');
|
LS.toast(checked ? 'Модуль симуляций включён' : 'Модуль симуляций отключён', checked ? 'success' : 'warning');
|
||||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function simToggleOne(simId, enabled) {
|
async function simToggleOne(simId, enabled) {
|
||||||
const dis = new Set(_simsSettings.disabled_ids || []);
|
|
||||||
if (enabled) dis.delete(simId); else dis.add(simId);
|
|
||||||
const disabled_ids = [...dis];
|
|
||||||
try {
|
try {
|
||||||
await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ disabled_ids }) });
|
await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ enabled }) });
|
||||||
_simsSettings.disabled_ids = disabled_ids;
|
const s = _sims.find(x => x.id === simId);
|
||||||
|
if (s) s.enabled = enabled;
|
||||||
const card = document.getElementById('simcard-' + simId);
|
const card = document.getElementById('simcard-' + simId);
|
||||||
if (card) card.classList.toggle('enabled', enabled);
|
if (card) card.classList.toggle('enabled', enabled);
|
||||||
LS.toast(enabled ? `«${simId}» включена` : `«${simId}» отключена`, enabled ? 'success' : 'warning');
|
LS.toast(enabled ? `«${simId}» включена` : `«${simId}» отключена`, enabled ? 'success' : 'warning');
|
||||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simToggleFeatured(simId, featured) {
|
||||||
|
try {
|
||||||
|
await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ featured }) });
|
||||||
|
const s = _sims.find(x => x.id === simId);
|
||||||
|
if (s) s.featured = featured;
|
||||||
|
_render();
|
||||||
|
LS.toast(featured ? `«${simId}» в рекомендуемых` : `«${simId}» убрана из рекомендуемых`, 'success');
|
||||||
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Фаза 5: редактор курикулумных связей (inline-панель под карточкой) ── */
|
||||||
|
|
||||||
|
async function _ensureTextbooks() {
|
||||||
|
if (_textbooks) return _textbooks;
|
||||||
|
try {
|
||||||
|
const data = await LS.api('/api/access/catalog');
|
||||||
|
_textbooks = (data && data.textbooks) || [];
|
||||||
|
} catch (e) { _textbooks = []; }
|
||||||
|
return _textbooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simToggleLinks(simId) {
|
||||||
|
const panel = document.getElementById('simlinks-' + simId);
|
||||||
|
if (!panel) return;
|
||||||
|
if (panel.style.display !== 'none') { panel.style.display = 'none'; return; }
|
||||||
|
panel.style.display = 'block';
|
||||||
|
panel.innerHTML = '<div style="font-size:.72rem;color:var(--text-3)">Загрузка связей…</div>';
|
||||||
|
try {
|
||||||
|
const [rel] = await Promise.all([
|
||||||
|
LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related'),
|
||||||
|
_ensureTextbooks(),
|
||||||
|
]);
|
||||||
|
_renderLinksPanel(simId, rel);
|
||||||
|
} catch (e) {
|
||||||
|
panel.innerHTML = '<div style="font-size:.72rem;color:var(--pink,#f15bb5)">Ошибка: ' + esc(e.message) + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderLinksPanel(simId, rel) {
|
||||||
|
const panel = document.getElementById('simlinks-' + simId);
|
||||||
|
if (!panel) return;
|
||||||
|
const links = (rel && rel.links) || {};
|
||||||
|
const tb = links.textbook || [];
|
||||||
|
|
||||||
|
let html = '<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);margin-bottom:6px">Связи с учебниками</div>';
|
||||||
|
|
||||||
|
if (tb.length) {
|
||||||
|
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
|
||||||
|
tb.forEach(l => {
|
||||||
|
html += `<span style="display:inline-flex;align-items:center;gap:6px;font-size:.72rem;padding:3px 6px 3px 10px;border-radius:999px;background:rgba(155,93,229,.14);color:var(--violet);border:1px solid rgba(155,93,229,.3)">
|
||||||
|
${esc(l.label || l.ref_id)}
|
||||||
|
<button title="Удалить связь" onclick="simDelLink('${esc(simId)}', ${Number(l.id)})"
|
||||||
|
style="background:none;border:none;cursor:pointer;color:inherit;padding:0;line-height:1;font-size:.95rem;opacity:.7">×</button>
|
||||||
|
</span>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
} else {
|
||||||
|
html += '<div style="font-size:.72rem;color:var(--text-3);margin-bottom:8px">Пока нет связей</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// add-form: textbook <select> + button
|
||||||
|
const linkedSlugs = new Set(tb.map(l => l.ref_id));
|
||||||
|
const opts = (_textbooks || [])
|
||||||
|
.filter(t => !linkedSlugs.has(t.slug))
|
||||||
|
.map(t => `<option value="${esc(t.slug)}">${esc(t.title)}</option>`).join('');
|
||||||
|
html += `<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||||||
|
<select id="simlink-sel-${esc(simId)}" style="flex:1;min-width:180px;font-size:.75rem;padding:5px 8px;border-radius:8px;background:var(--bg-2,#1a1a2e);color:var(--text);border:1px solid var(--border,rgba(255,255,255,.14))">
|
||||||
|
<option value="">— выбрать учебник —</option>${opts}
|
||||||
|
</select>
|
||||||
|
<button onclick="simAddLink('${esc(simId)}')"
|
||||||
|
style="font-size:.75rem;padding:5px 12px;border-radius:8px;border:1px solid var(--violet);background:rgba(155,93,229,.15);color:var(--violet);cursor:pointer">Добавить</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
panel.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simAddLink(simId) {
|
||||||
|
const sel = document.getElementById('simlink-sel-' + simId);
|
||||||
|
const slug = sel && sel.value;
|
||||||
|
if (!slug) { LS.toast('Выберите учебник', 'warning'); return; }
|
||||||
|
try {
|
||||||
|
await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links',
|
||||||
|
{ method: 'POST', body: JSON.stringify({ kind: 'textbook', ref_id: slug }) });
|
||||||
|
LS.toast('Связь добавлена', 'success');
|
||||||
|
const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related');
|
||||||
|
_renderLinksPanel(simId, rel);
|
||||||
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simDelLink(simId, linkId) {
|
||||||
|
try {
|
||||||
|
await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links/' + linkId, { method: 'DELETE' });
|
||||||
|
LS.toast('Связь удалена', 'success');
|
||||||
|
const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related');
|
||||||
|
_renderLinksPanel(simId, rel);
|
||||||
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
window.simsMasterToggle = simsMasterToggle;
|
window.simsMasterToggle = simsMasterToggle;
|
||||||
window.simToggleOne = simToggleOne;
|
window.simToggleOne = simToggleOne;
|
||||||
|
window.simToggleFeatured = simToggleFeatured;
|
||||||
|
window.simToggleLinks = simToggleLinks;
|
||||||
|
window.simAddLink = simAddLink;
|
||||||
|
window.simDelLink = simDelLink;
|
||||||
|
|
||||||
window.AdminSections = window.AdminSections || {};
|
window.AdminSections = window.AdminSections || {};
|
||||||
window.AdminSections.sims = {
|
window.AdminSections.sims = {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/* 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 = '<b>' + p.eq + '</b><br><span style="color:var(--muted)">' + p.given + '</span><div style="margin-top:8px">';
|
||||||
|
for (var i = 0; i < shown; i++) html += '<div class="def-box" style="margin:6px 0">' + p.steps[i] + '</div>';
|
||||||
|
if (shown === 0) html += '<span style="color:var(--muted)">Нажмите «Следующий шаг».</span>';
|
||||||
|
html += '</div>'; 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §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 = { final1: mount_final1, p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p22: mount_p22, p23: mount_p23 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/* chem8_ch2_widgets.js — виджеты Главы 2 «Периодический закон и ПСХЭ».
|
||||||
|
* Использует window.Chem8: miniPeriodic, testTube.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
function C() { return W.Chem8 || {}; }
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
|
||||||
|
/* интерактивная ПСХЭ с кнопками-режимами подсветки */
|
||||||
|
function periodicModes(mountId, modes) {
|
||||||
|
var el = $(mountId); if (!el || el._b || !C().miniPeriodic) return; el._b = 1;
|
||||||
|
var bar = document.createElement('div'); bar.className = 'pt-modes';
|
||||||
|
var grid = document.createElement('div');
|
||||||
|
modes.forEach(function (m, i) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
bar.appendChild(b);
|
||||||
|
});
|
||||||
|
el.appendChild(bar); el.appendChild(grid);
|
||||||
|
var api = C().miniPeriodic(grid, {});
|
||||||
|
var legend = document.createElement('div'); legend.className = 'pt-legend';
|
||||||
|
legend.innerHTML = '<span><i style="background:rgba(13,148,136,.4)"></i>металлы</span><span><i style="background:rgba(245,158,11,.5)"></i>неметаллы</span><span><i style="background:rgba(124,58,237,.4)"></i>металлоиды</span><span><i style="background:rgba(37,99,235,.4)"></i>инертные</span>';
|
||||||
|
el.appendChild(legend);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount_p24() {
|
||||||
|
periodicModes('c-pt-metals', [
|
||||||
|
{ k: 'metals', l: 'Металлы' }, { k: 'nonmetals', l: 'Неметаллы' }, { k: 'metalloids', l: 'Металлоиды' }, { k: null, l: 'Сброс' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function mount_p26() {
|
||||||
|
periodicModes('c-pt-fam', [
|
||||||
|
{ k: 'alkali', l: 'Щелочные' }, { k: 'alkaline', l: 'Щёлочноземельные' }, { k: 'halogens', l: 'Галогены' }, { k: 'noble', l: 'Инертные газы' }, { k: null, l: 'Сброс' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function mount_p28() {
|
||||||
|
periodicModes('c-pt-struct', [
|
||||||
|
{ k: { period: 2 }, l: 'Период 2' }, { k: { period: 3 }, l: 'Период 3' }, { k: { group: 1 }, l: 'Группа I' }, { k: { group: 17 }, l: 'Группа VII' }, { k: null, l: 'Сброс' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §25 — амфотерность: Zn(OH)₂ растворяется и в кислоте, и в щёлочи */
|
||||||
|
function mount_p25() {
|
||||||
|
var el = $('c-amph'); if (!el || el._b) return; el._b = 1;
|
||||||
|
el.innerHTML =
|
||||||
|
'<div class="amph-row">' +
|
||||||
|
'<button class="btn amph-btn" data-r="acid">+ кислота (HCl)</button>' +
|
||||||
|
'<button class="btn amph-btn" data-r="base">+ щёлочь (NaOH)</button>' +
|
||||||
|
'<button class="btn amph-reset">Сначала</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="amph-stage"></div>' +
|
||||||
|
'<div class="out amph-out">Zn(OH)₂ — амфотерный гидроксид. Добавь кислоту или щёлочь и посмотри, что будет.</div>';
|
||||||
|
var stage = el.querySelector('.amph-stage'), out = el.querySelector('.amph-out');
|
||||||
|
function tt(o) { return C().testTube ? C().testTube(o) : ''; }
|
||||||
|
function reset() { stage.innerHTML = '<div style="text-align:center;color:#0f766e">' + tt({ color: '#fff', precipitate: '#cbd5e1', label: 'Zn(OH)2' }) + '</div><div class="tt-cap" style="margin:0 auto">Белый осадок Zn(OH)₂</div>'; out.className = 'out amph-out'; out.innerHTML = 'Zn(OH)₂ — белый студенистый осадок (амфотерный гидроксид).'; }
|
||||||
|
el.querySelectorAll('.amph-btn').forEach(function (b) {
|
||||||
|
b.addEventListener('click', function () {
|
||||||
|
var r = b.getAttribute('data-r');
|
||||||
|
stage.innerHTML = '<div style="text-align:center;color:#0f766e">' + tt({ color: '#dbeafe' }) + '</div><div class="tt-cap" style="margin:0 auto">Осадок растворился</div>';
|
||||||
|
out.className = 'out amph-out ok';
|
||||||
|
out.innerHTML = r === 'acid'
|
||||||
|
? 'Как <b>основание</b>: Zn(OH)₂ + 2HCl → ZnCl₂ + 2H₂O — осадок растворился.'
|
||||||
|
: 'Как <b>кислота</b>: Zn(OH)₂ + 2NaOH → Na₂[Zn(OH)₄] — осадок растворился (амфотерность!).';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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 = { final1: mount_final1, p24: mount_p24, p26: mount_p26, p28: mount_p28 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/* 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) {}
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
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 = { final1: mount_final1, p34: mount_p34, p35: mount_p35 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/* chem8_ch4_widgets.js — виджеты Главы 4 «Химическая связь».
|
||||||
|
* Использует window.Chem8: bondType.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
function C() { return W.Chem8 || {}; }
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
|
||||||
|
function M() { return W.Chem8Mol; }
|
||||||
|
function mount_p37() { var el = $('c-bond1'); if (el && !el._b && C().bondType) { el._b = 1; C().bondType(el, { a: 'H', b: 'H' }); } }
|
||||||
|
function mount_p38() {
|
||||||
|
var el = $('c-bond2'); if (el && !el._b && C().bondType) { el._b = 1; C().bondType(el, { a: 'H', b: 'Cl' }); }
|
||||||
|
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 = { final1: mount_final1, p37: mount_p37, p38: mount_p38, p41: mount_p41 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* 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 = '<b>' + p.eq + '</b><div style="margin-top:8px">';
|
||||||
|
for (var i = 0; i < shown; i++) html += '<div class="def-box" style="margin:6px 0">' + p.steps[i] + '</div>';
|
||||||
|
if (shown === 0) html += '<span style="color:var(--muted)">Нажимай «Следующий шаг» — разберём метод электронного баланса.</span>';
|
||||||
|
html += '</div>'; 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();
|
||||||
|
}
|
||||||
|
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 = { final1: mount_final1, p44: mount_p44 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/* chem8_ch6_widgets.js — виджеты Главы 6 «Растворы».
|
||||||
|
* Использует window.Chem8: classifier, solubilityTable, molarMass.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
function C() { return W.Chem8 || {}; }
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
function rr(v, d) { var p = Math.pow(10, d == null ? 2 : d); return (Math.round(v * p) / p).toString().replace('.', ','); }
|
||||||
|
|
||||||
|
/* §46 — классификатор смесей */
|
||||||
|
function mount_p46() {
|
||||||
|
var el = $('c-mix'); if (!el || el._b || !C().classifier) return; el._b = 1;
|
||||||
|
C().classifier(el, {
|
||||||
|
items: [
|
||||||
|
{ id: 'air', label: 'воздух', cat: 'odn' }, { id: 'saltsol', label: 'раствор соли', cat: 'odn' }, { id: 'steel', label: 'сталь', cat: 'odn' },
|
||||||
|
{ id: 'sandwater', label: 'песок + вода', cat: 'neod' }, { id: 'milk', label: 'молоко', cat: 'neod' }, { id: 'granite', label: 'гранит', cat: 'neod' }
|
||||||
|
],
|
||||||
|
buckets: [{ cat: 'odn', label: 'Однородные (растворы)' }, { cat: 'neod', label: 'Неоднородные' }],
|
||||||
|
onCheck: function (ok) { if (ok && W.addXp) W.addXp(8, 'p46-mix'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §48 — кривая растворимости s = f(t) */
|
||||||
|
var CURVE = { KNO3: [13, 21, 32, 46, 64, 88, 110, 138, 169, 202, 246], NaCl: [35.7, 35.8, 36, 36.3, 36.6, 37, 37.3, 37.8, 38.4, 39, 39.8] };
|
||||||
|
function mount_p48() {
|
||||||
|
var el = $('c-solcurve'); if (!el || el._b) return; el._b = 1;
|
||||||
|
el.innerHTML = '<div class="fld"><label>Вещество</label><select id="sc-sub"><option value="KNO3">KNO₃ (нитрат калия)</option><option value="NaCl">NaCl (соль)</option></select><label>t, °C</label><input type="range" id="sc-t" min="0" max="100" step="10" value="40"><span class="bd" id="sc-tv"></span></div><div id="sc-plot"></div><div class="out" id="sc-out"></div>';
|
||||||
|
var sub = $('sc-sub'), tr = $('sc-t'), tv = $('sc-tv'), plot = $('sc-plot'), out = $('sc-out');
|
||||||
|
function draw() {
|
||||||
|
var data = CURVE[sub.value], t = +tr.value, idx = t / 10, s = data[idx];
|
||||||
|
tv.textContent = t + ' °C';
|
||||||
|
var maxS = Math.max.apply(null, CURVE.KNO3);
|
||||||
|
var W0 = 280, H0 = 140, pad = 24;
|
||||||
|
var pts = data.map(function (v, i) { var x = pad + i / 10 * (W0 - pad * 2); var y = H0 - pad - v / maxS * (H0 - pad * 2); return x.toFixed(1) + ',' + y.toFixed(1); }).join(' ');
|
||||||
|
var cx = pad + idx / 10 * (W0 - pad * 2), cy = H0 - pad - s / maxS * (H0 - pad * 2);
|
||||||
|
plot.innerHTML = '<svg viewBox="0 0 ' + W0 + ' ' + H0 + '" style="width:100%;max-width:340px;color:var(--pri)">'
|
||||||
|
+ '<line x1="' + pad + '" y1="' + (H0 - pad) + '" x2="' + (W0 - pad) + '" y2="' + (H0 - pad) + '" stroke="currentColor" opacity=".4"/>'
|
||||||
|
+ '<line x1="' + pad + '" y1="' + pad + '" x2="' + pad + '" y2="' + (H0 - pad) + '" stroke="currentColor" opacity=".4"/>'
|
||||||
|
+ '<polyline points="' + pts + '" fill="none" stroke="var(--pri)" stroke-width="2"/>'
|
||||||
|
+ '<circle cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="5" fill="var(--pri)"/>'
|
||||||
|
+ '<text x="' + (W0 - pad) + '" y="' + (H0 - 6) + '" text-anchor="end" font-size="9" fill="currentColor">t, °C</text>'
|
||||||
|
+ '<text x="' + pad + '" y="' + (pad - 8) + '" font-size="9" fill="currentColor">s, г/100г</text>'
|
||||||
|
+ '</svg>';
|
||||||
|
out.className = 'out ok';
|
||||||
|
out.innerHTML = '<span class="bd">При ' + t + ' °C растворимость ' + sub.value + ' ≈ <b>' + rr(s, 1) + ' г</b> на 100 г воды.' + (sub.value === 'KNO3' ? ' Растворимость сильно растёт с температурой.' : ' У NaCl почти не зависит от t.') + '</span>';
|
||||||
|
}
|
||||||
|
sub.addEventListener('change', draw); tr.addEventListener('input', draw); draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §50 — массовая доля w */
|
||||||
|
function mount_p50() {
|
||||||
|
var el = $('c-wcalc'); if (!el || el._b) return; el._b = 1;
|
||||||
|
el.innerHTML = '<div class="fld"><label>m(вещества), г</label><input type="number" id="w-ms" value="20" style="width:80px"><label>m(воды), г</label><input type="number" id="w-mw" value="80" style="width:80px"><button class="btn primary" id="w-go">Найти w</button></div><div class="out" id="w-out"></div>';
|
||||||
|
function calc() {
|
||||||
|
var ms = parseFloat($('w-ms').value), mw = parseFloat($('w-mw').value);
|
||||||
|
if (isNaN(ms) || isNaN(mw) || ms + mw <= 0) { $('w-out').className = 'out bad'; $('w-out').textContent = 'Введите массы.'; return; }
|
||||||
|
var w = ms / (ms + mw) * 100;
|
||||||
|
$('w-out').className = 'out ok';
|
||||||
|
$('w-out').innerHTML = '<span class="bd">m(раствора) = ' + ms + ' + ' + mw + ' = ' + (ms + mw) + ' г<br>w = m(в-ва)/m(р-ра) = ' + ms + '/' + (ms + mw) + ' = <b>' + rr(w, 1) + ' %</b></span>';
|
||||||
|
}
|
||||||
|
$('w-go').addEventListener('click', calc); calc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §51 — молярная концентрация c = n/V */
|
||||||
|
function mount_p51() {
|
||||||
|
var el = $('c-ccalc'); if (!el || el._b) return; el._b = 1;
|
||||||
|
el.innerHTML = '<div class="fld"><label>Вещество</label><input type="text" id="c-f" value="NaOH" style="width:110px;font-family:var(--mono)"><label>m, г</label><input type="number" id="c-m" value="40" style="width:70px"><label>V, л</label><input type="number" id="c-v" value="1" step="0.1" style="width:70px"><button class="btn primary" id="c-go">Найти c</button></div><div class="out" id="c-out"></div>';
|
||||||
|
function calc() {
|
||||||
|
var f = $('c-f').value.trim(), M = C().molarMass ? C().molarMass(f) : NaN, m = parseFloat($('c-m').value), V = parseFloat($('c-v').value);
|
||||||
|
if (isNaN(M)) { $('c-out').className = 'out bad'; $('c-out').textContent = 'Не удалось разобрать формулу.'; return; }
|
||||||
|
if (isNaN(m) || isNaN(V) || V <= 0) { $('c-out').className = 'out bad'; $('c-out').textContent = 'Введите m и V.'; return; }
|
||||||
|
var n = m / M, c = n / V;
|
||||||
|
$('c-out').className = 'out ok';
|
||||||
|
$('c-out').innerHTML = '<span class="bd">M(' + f + ') = ' + M + ' г/моль<br>n = m/M = ' + m + '/' + M + ' = ' + rr(n) + ' моль<br>c = n/V = ' + rr(n) + '/' + rr(V) + ' = <b>' + rr(c) + ' моль/л</b></span>';
|
||||||
|
}
|
||||||
|
$('c-go').addEventListener('click', calc); calc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §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 = { final1: mount_final1, p47: mount_p47, p48: mount_p48 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
/* chem8_engine.js — общий движок интерактивных учебников «Химия 8».
|
||||||
|
*
|
||||||
|
* Воспроизводит каркас учебников физики: SPA с para-selector, ленивая сборка §,
|
||||||
|
* карточки теории (makeCard), тренажёр задач (числовой ввод + MCQ), sidebar-шпаргалка,
|
||||||
|
* прогресс/XP/уровни/достижения, серверная синхронизация прогресса, тема.
|
||||||
|
*
|
||||||
|
* Страница главы ОБЪЯВЛЯЕТ данные (до загрузки движка, инлайн-скриптом):
|
||||||
|
* window.CHEM8_CFG = { slug, themeKey, xpKey, progKey, achKey, hubHref }
|
||||||
|
* window.PARAS = [{id, num, name, sub, final?}]
|
||||||
|
* window.BUILDERS = { p1: ()=>build_p1(), ... } // наполняют #<id>-body
|
||||||
|
* window.POOLS = { p1: [task,...], ... } // task: {q,hint,unit,a,ex,tol} | {q,opts,a,ex}
|
||||||
|
* window.SIDEBARS = { p1: {title, rows:[[k,v],...]}, ... }
|
||||||
|
* window.TIPS = [{sec, html}, ...]
|
||||||
|
* window.CHEM8_WIDGETS = { p1: ()=>add_p1(), ... } // монтаж виджетов §
|
||||||
|
* window.FLAG_MOUNTS = { p6: ()=>mountFlag('p6'), ... } // флагман-интерактивы
|
||||||
|
* window.ACH_LABELS = { start, p1_done, ... }
|
||||||
|
*
|
||||||
|
* Движок ЭКСПОРТИРУЕТ на window: goTo, checkNum, selectMcq, nextTask, goToTask,
|
||||||
|
* resetTasks, makeCard, secNav, readButton, addXp, achievement, bumpProgress.
|
||||||
|
* Инициализация — на DOMContentLoaded.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Конфиг резолвится лениво в init() — страница задаёт window.CHEM8_CFG
|
||||||
|
// в body-скрипте, который при defer выполняется до движка, но не полагаемся на это.
|
||||||
|
var CFG = {}, SLUG = 'chemistry-8';
|
||||||
|
var K = { theme: 'chemistry8_theme', xp: 'chemistry8_xp', prog: 'chemistry-8_progress', ach: 'chemistry-8_ach' };
|
||||||
|
function resolveCfg() {
|
||||||
|
CFG = W.CHEM8_CFG || {};
|
||||||
|
SLUG = CFG.slug || 'chemistry-8';
|
||||||
|
K = {
|
||||||
|
theme: CFG.themeKey || 'chemistry8_theme',
|
||||||
|
xp: CFG.xpKey || 'chemistry8_xp',
|
||||||
|
prog: CFG.progKey || (SLUG + '_progress'),
|
||||||
|
ach: CFG.achKey || (SLUG + '_ach')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function PARAS() { return W.PARAS || []; }
|
||||||
|
function POOLS() { return W.POOLS || {}; }
|
||||||
|
function BUILDERS(){ return W.BUILDERS || {}; }
|
||||||
|
function ACHL() { return W.ACH_LABELS || {}; }
|
||||||
|
|
||||||
|
var STATE = { current: null, progress: {}, achievements: new Map(), xp: 0, level: 1 };
|
||||||
|
var SEC = {}; // STATE задач по секциям
|
||||||
|
|
||||||
|
/* ── XP / уровни ───────────────────────────────────────────────── */
|
||||||
|
function calcLevel(xp) { return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
|
||||||
|
function xpForLevel(lv) { return (lv - 1) * (lv - 1) * 100; }
|
||||||
|
|
||||||
|
function loadProgress() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem(K.prog); if (s) Object.assign(STATE.progress, JSON.parse(s));
|
||||||
|
var a = localStorage.getItem(K.ach);
|
||||||
|
if (a) { var p = JSON.parse(a); if (p && typeof p === 'object') for (var id in p) STATE.achievements.set(id, p[id]); }
|
||||||
|
STATE.xp = parseInt(localStorage.getItem(K.xp) || '0', 10) || 0;
|
||||||
|
STATE.level = calcLevel(STATE.xp);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
function saveProgress() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(K.prog, JSON.stringify(STATE.progress));
|
||||||
|
localStorage.setItem(K.ach, JSON.stringify(mapToObj(STATE.achievements)));
|
||||||
|
localStorage.setItem(K.xp, String(STATE.xp));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
function mapToObj(m) { var o = {}; m.forEach(function (v, k) { o[k] = v; }); return o; }
|
||||||
|
|
||||||
|
function addXp(n, src) {
|
||||||
|
if (!n) return;
|
||||||
|
var prev = STATE.level;
|
||||||
|
STATE.xp = Math.max(0, (STATE.xp || 0) + n); STATE.level = calcLevel(STATE.xp);
|
||||||
|
saveProgress(); refreshUI();
|
||||||
|
try { if (W.LS && W.LS.xp && W.LS.xp.add) W.LS.xp.add(n, SLUG + '-' + (src || 'x')); } catch (e) {}
|
||||||
|
if (STATE.level > prev) popup('Уровень ' + STATE.level + '!');
|
||||||
|
}
|
||||||
|
function bumpProgress(key, delta) {
|
||||||
|
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta));
|
||||||
|
saveProgress(); refreshUI();
|
||||||
|
if (STATE.progress[key] >= 50) markServerRead(key);
|
||||||
|
}
|
||||||
|
function achievement(id, text) {
|
||||||
|
if (STATE.achievements.has(id)) return;
|
||||||
|
var label = text || ACHL()[id] || id;
|
||||||
|
STATE.achievements.set(id, label); saveProgress();
|
||||||
|
popup(label, true);
|
||||||
|
addXp(20, 'ach-' + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── серверная синхронизация ───────────────────────────────────── */
|
||||||
|
var _marked = {}, _pending = null, _timer = null;
|
||||||
|
function _flush() {
|
||||||
|
var body = _pending; _pending = null; if (!body) return;
|
||||||
|
var tok = (W.LS && W.LS.getToken) ? W.LS.getToken() : ''; if (!tok) return;
|
||||||
|
fetch('/api/textbooks/' + SLUG + '/progress', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok },
|
||||||
|
body: JSON.stringify(body), keepalive: true
|
||||||
|
}).catch(function () {});
|
||||||
|
}
|
||||||
|
function _queue(p) { _pending = Object.assign(_pending || {}, p); if (_timer) clearTimeout(_timer); _timer = setTimeout(_flush, 600); }
|
||||||
|
function markServerRead(id) { if (_marked[id] || /^final/.test(id)) return; _marked[id] = 1; _queue({ mark_read: id }); }
|
||||||
|
function markLastPara(id) { _queue({ last_para: id }); }
|
||||||
|
function loadServerReadState() {
|
||||||
|
var tok = (W.LS && W.LS.getToken) ? W.LS.getToken() : ''; if (!tok) return;
|
||||||
|
fetch('/api/textbooks/' + SLUG, { headers: { 'Authorization': 'Bearer ' + tok } })
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (d) {
|
||||||
|
if (!d || !d.progress || !d.progress.read) return;
|
||||||
|
d.progress.read.forEach(function (k) { _marked[k] = 1; if ((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; });
|
||||||
|
saveProgress(); refreshUI();
|
||||||
|
}).catch(function () {});
|
||||||
|
}
|
||||||
|
W.addEventListener('beforeunload', _flush);
|
||||||
|
|
||||||
|
/* ── popup ачивки / уровня ─────────────────────────────────────── */
|
||||||
|
function popup(text, gold) {
|
||||||
|
var pop = document.getElementById('ach-popup'); if (!pop) return;
|
||||||
|
var t = document.getElementById('ach-text'); if (t) t.textContent = text;
|
||||||
|
pop.classList.toggle('gold', !!gold);
|
||||||
|
pop.classList.add('show'); setTimeout(function () { pop.classList.remove('show'); }, 3000);
|
||||||
|
if (gold) { try { if (W.confetti) W.confetti({ particleCount: 160, spread: 95, origin: { y: .65 } }); } catch (e) {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── para-selector + hero ──────────────────────────────────────── */
|
||||||
|
function buildParaSelector() {
|
||||||
|
var g = document.getElementById('psel-grid'); if (!g) return;
|
||||||
|
g.innerHTML = '';
|
||||||
|
PARAS().forEach(function (p) {
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.className = 'psel-card' + (p.final ? ' final' : '');
|
||||||
|
card.dataset.id = p.id; card.dataset.progCard = p.id;
|
||||||
|
card.innerHTML = '<div class="psel-num">' + p.num + '</div><div class="psel-name">' + p.name + '</div>'
|
||||||
|
+ (p.sub ? '<div class="psel-sub">' + p.sub + '</div>' : '')
|
||||||
|
+ '<div class="psel-prog"><div class="psel-prog-fill"></div></div>'
|
||||||
|
+ '<span class="psel-done"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>';
|
||||||
|
card.addEventListener('click', function () { goTo(p.id); });
|
||||||
|
g.appendChild(card);
|
||||||
|
});
|
||||||
|
if (W.renderMathInElement) try { renderMath(g); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUI() {
|
||||||
|
var total = PARAS().length || 1;
|
||||||
|
var sum = 0; PARAS().forEach(function (p) { sum += (STATE.progress[p.id] || 0); });
|
||||||
|
var pct = Math.round(sum / total);
|
||||||
|
var hf = document.getElementById('hero-hp-fill'); if (hf) hf.style.width = pct + '%';
|
||||||
|
var ht = document.getElementById('hero-hp-text'); if (ht) ht.textContent = pct + '%';
|
||||||
|
var xb = document.getElementById('hero-xp-badge');
|
||||||
|
if (xb) xb.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. ' + STATE.level + ' \xb7 ' + (STATE.xp || 0) + ' XP';
|
||||||
|
document.querySelectorAll('.psel-card').forEach(function (c) {
|
||||||
|
var id = c.dataset.id; var pp = STATE.progress[id] || 0;
|
||||||
|
var fl = c.querySelector('.psel-prog-fill'); if (fl) fl.style.width = pp + '%';
|
||||||
|
c.classList.toggle('done', pp >= 50);
|
||||||
|
});
|
||||||
|
if (STATE.current && document.getElementById('sidebar-content')) { try { buildSidebar(STATE.current); } catch (e) {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── ленивая сборка § + инъекция задач ─────────────────────────── */
|
||||||
|
var BUILT = {};
|
||||||
|
function ensureBuilt(id) {
|
||||||
|
if (BUILT[id]) return;
|
||||||
|
var fn = BUILDERS()[id];
|
||||||
|
if (fn) { try { fn(); } catch (e) { if (W.console) console.warn('build ' + id, e.message); } BUILT[id] = 1; }
|
||||||
|
_injectTasks(id);
|
||||||
|
_mountWidgets(id);
|
||||||
|
}
|
||||||
|
function _mountWidgets(id) {
|
||||||
|
setTimeout(function () {
|
||||||
|
try { if (W.CHEM8_WIDGETS && W.CHEM8_WIDGETS[id]) W.CHEM8_WIDGETS[id](); } catch (e) { if (W.console) console.warn('widget ' + id, e.message); }
|
||||||
|
try { if (W.FLAG_MOUNTS && W.FLAG_MOUNTS[id]) W.FLAG_MOUNTS[id](); } catch (e) { if (W.console) console.warn('flag ' + id, e.message); }
|
||||||
|
}, 40);
|
||||||
|
}
|
||||||
|
function _makeTaskBlock(sec) {
|
||||||
|
return '<div class="legacy-tasks" id="ptab-' + sec + '">'
|
||||||
|
+ '<div class="lt-head"><span class="lt-title">Задачи параграфа</span>'
|
||||||
|
+ '<span class="chip chip-ok"><span id="ok' + sec + '">0</span> верно</span>'
|
||||||
|
+ '<span class="chip chip-tot"><span id="cur' + sec + '">0</span>/<span id="max' + sec + '">?</span></span>'
|
||||||
|
+ '<button class="btn lt-reset" onclick="resetTasks(\'' + sec + '\')">Заново</button></div>'
|
||||||
|
+ '<div class="prog-wrap"><div id="prog' + sec + '" class="prog-fill"></div></div>'
|
||||||
|
+ '<div class="nav-dots" id="navDots' + sec + '"></div>'
|
||||||
|
+ '<div id="taskArea' + sec + '"></div>'
|
||||||
|
+ '<div class="feedback" id="fb' + sec + '"></div>'
|
||||||
|
+ '<div class="lt-foot"><button class="btn primary" id="nextBtn' + sec + '" onclick="nextTask(\'' + sec + '\')" style="display:none">Следующая →</button></div>'
|
||||||
|
+ '<div class="summary" id="sum' + sec + '"><div class="sum-t">Параграф пройден!</div><div class="big-score" id="sumScore' + sec + '"></div><div class="sum-grade" id="sumGrade' + sec + '"></div></div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
function _injectTasks(id) {
|
||||||
|
var pool = POOLS()[id]; if (!pool) return;
|
||||||
|
var body = document.getElementById(id + '-body'); if (!body || body.querySelector('.legacy-tasks')) return;
|
||||||
|
if (!SEC[id]) SEC[id] = { idx: 0, results: pool.map(function () { return null; }), selections: pool.map(function () { return null; }), answered: false };
|
||||||
|
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
|
||||||
|
setTimeout(function () { try { renderTask(id); } catch (e) {} }, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── навигация по § ────────────────────────────────────────────── */
|
||||||
|
function goTo(id) {
|
||||||
|
STATE.current = id; ensureBuilt(id);
|
||||||
|
document.querySelectorAll('.sec').forEach(function (s) { s.classList.remove('active'); });
|
||||||
|
var el = document.getElementById('sec-' + id); if (el) el.classList.add('active');
|
||||||
|
document.querySelectorAll('.psel-card').forEach(function (c) { c.classList.toggle('active', c.dataset.id === id); });
|
||||||
|
buildSidebar(id);
|
||||||
|
try { W.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) {}
|
||||||
|
if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10);
|
||||||
|
if (W.renderMathInElement && el) setTimeout(function () { renderMath(el); }, 0);
|
||||||
|
markLastPara(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── sidebar ───────────────────────────────────────────────────── */
|
||||||
|
function buildSidebar(id) {
|
||||||
|
var box = document.getElementById('sidebar-content'); if (!box) return;
|
||||||
|
var SB = W.SIDEBARS || {}; var sb = SB[id] || SB[(PARAS()[0] || {}).id] || { title: '', rows: [] };
|
||||||
|
var xpLv = xpForLevel(STATE.level), xpNext = xpForLevel(STATE.level + 1);
|
||||||
|
var pct = (xpNext - xpLv) > 0 ? Math.round((STATE.xp - xpLv) / (xpNext - xpLv) * 100) : 100;
|
||||||
|
var html = '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div>'
|
||||||
|
+ '<div class="xp-bar"><div class="xp-fill" style="width:' + pct + '%"></div></div>'
|
||||||
|
+ '<div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
|
||||||
|
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
|
||||||
|
sb.rows.forEach(function (r) { html += '<div class="sidecard-row"><b>' + r[0] + '</b>' + (r[1] ? ' — ' + r[1] : '') + '</div>'; });
|
||||||
|
html += '</div>';
|
||||||
|
var tips = W.TIPS || []; var tip = tips.filter(function (t) { return t.sec === id; })[0] || tips[0];
|
||||||
|
if (tip) html += '<div class="sidecard tip"><h4><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 20 2 20"/></svg> Подсказка</h4><div class="sidecard-row" style="font-size:.84rem;line-height:1.55">' + tip.html + '</div></div>';
|
||||||
|
if (STATE.achievements.size > 0) {
|
||||||
|
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||||
|
var vals = []; STATE.achievements.forEach(function (v) { vals.push(v); });
|
||||||
|
vals.slice(-4).forEach(function (t) { html += '<div class="sidecard-row done">✓ ' + t + '</div>'; });
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML = html;
|
||||||
|
if (W.renderMathInElement) try { renderMath(box); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── карточки / навигация / кнопка прочтения ───────────────────── */
|
||||||
|
var ICONS = {
|
||||||
|
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
||||||
|
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
|
||||||
|
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
|
||||||
|
lab: '<svg class="ic" viewBox="0 0 24 24"><path d="M10 2v7.5L4.5 19a2 2 0 0 0 1.7 3h11.6a2 2 0 0 0 1.7-3L14 9.5V2"/><line x1="9" y1="2" x2="15" y2="2"/></svg>'
|
||||||
|
};
|
||||||
|
function makeCard(kind, title, num, body) {
|
||||||
|
var labels = { theory: 'Теория', example: 'Пример', rule: 'Правило', lab: 'Практика' };
|
||||||
|
return '<div class="card"><div class="card-header"><div class="card-icon ' + kind + '">' + (ICONS[kind] || ICONS.theory) + '</div>'
|
||||||
|
+ '<div class="card-title">' + (labels[kind] || '') + (title && title !== labels[kind] ? ' \xb7 ' + title : '') + '</div>'
|
||||||
|
+ (num ? '<div class="card-num">' + num + '</div>' : '') + '</div><div class="card-body">' + body + '</div></div>';
|
||||||
|
}
|
||||||
|
function paraName(id) { var p = PARAS().filter(function (x) { return x.id === id; })[0]; return p ? p.num : id; }
|
||||||
|
function secNav(prev, next) {
|
||||||
|
var h = '<div class="sec-nav">';
|
||||||
|
h += prev ? '<button class="btn" onclick="goTo(\'' + prev + '\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ' + paraName(prev) + '</button>' : '<span></span>';
|
||||||
|
h += next ? '<button class="btn primary" onclick="goTo(\'' + next + '\')">' + paraName(next) + ' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>' : '<span></span>';
|
||||||
|
return h + '</div>';
|
||||||
|
}
|
||||||
|
function readButton(paraId) {
|
||||||
|
var p = PARAS().filter(function (x) { return x.id === paraId; })[0];
|
||||||
|
var tail = p && p.final ? 'финал' : (p ? p.num : '?');
|
||||||
|
return '<div class="read-wrap"><button class="btn primary" id="' + paraId + '-read-btn">'
|
||||||
|
+ '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> Я изучил — ' + tail + ' (+10 XP)</button></div>';
|
||||||
|
}
|
||||||
|
function wireReadBtn(paraId) {
|
||||||
|
var btn = document.getElementById(paraId + '-read-btn'); if (!btn || btn._wired) return; btn._wired = 1;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
addXp(10, paraId + '-read'); bumpProgress(paraId, 30);
|
||||||
|
btn.textContent = 'Изучено! +10 XP'; btn.disabled = true; btn.style.opacity = .6;
|
||||||
|
var aId = paraId + '_done'; if (ACHL()[aId]) achievement(aId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMath(root) {
|
||||||
|
if (!W.renderMathInElement) return;
|
||||||
|
try { W.renderMathInElement(root, { delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
|
||||||
|
}
|
||||||
|
function doRender(el) { renderMath(el); }
|
||||||
|
|
||||||
|
/* ── ДВИЖОК ЗАДАЧ ──────────────────────────────────────────────── */
|
||||||
|
function renderTask(sec) {
|
||||||
|
var pool = POOLS()[sec], s = SEC[sec];
|
||||||
|
var area = document.getElementById('taskArea' + sec), fb = document.getElementById('fb' + sec), sum = document.getElementById('sum' + sec);
|
||||||
|
if (!area || !fb || !sum || !pool || !s) return;
|
||||||
|
sum.classList.remove('show');
|
||||||
|
var q = pool[s.idx], done = s.results[s.idx] !== null, isMcq = !!q.opts;
|
||||||
|
s.answered = done;
|
||||||
|
if (isMcq) {
|
||||||
|
var selIdx = s.selections[s.idx];
|
||||||
|
area.innerHTML = '<div class="task-card"><div class="task-num">Задача ' + (s.idx + 1) + ' из ' + pool.length + ' · Тест</div>'
|
||||||
|
+ '<div class="task-text">' + q.q + '</div><div class="mcq-opts">'
|
||||||
|
+ q.opts.map(function (opt, i) {
|
||||||
|
var cls = 'mcq-opt'; if (done) { if (i === q.a) cls += ' mcq-cor'; else if (i === selIdx) cls += ' mcq-wrong'; }
|
||||||
|
return '<button class="' + cls + '" id="mcqOpt' + sec + '_' + i + '" onclick="' + (done ? '' : 'selectMcq(\'' + sec + '\',' + i + ')') + '" ' + (done ? 'disabled' : '') + '><span class="mcq-let">' + String.fromCharCode(65 + i) + '.</span>' + opt + '</button>';
|
||||||
|
}).join('') + '</div></div>';
|
||||||
|
} else {
|
||||||
|
area.innerHTML = '<div class="task-card"><div class="task-num">Задача ' + (s.idx + 1) + ' из ' + pool.length + '</div>'
|
||||||
|
+ '<div class="task-text">' + q.q + '</div>'
|
||||||
|
+ (q.hint ? '<div class="task-hint"><svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg><span>' + q.hint + '</span></div>' : '')
|
||||||
|
+ '<div class="ans-row"><label>Ответ:</label><input class="ans-inp" type="text" id="ainp' + sec + '" placeholder="?" autocomplete="off"' + (done ? ' disabled' : '') + '>'
|
||||||
|
+ '<span class="unit-lbl">' + (q.unit || '') + '</span>'
|
||||||
|
+ (done ? '' : '<button class="btn primary" onclick="checkNum(\'' + sec + '\')">Проверить</button>') + '</div></div>';
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
var ok = s.results[s.idx];
|
||||||
|
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
|
||||||
|
fb.innerHTML = isMcq
|
||||||
|
? (ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.opts[q.a] + '</b>. ' + (q.ex || ''))
|
||||||
|
: (ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.a + ' ' + (q.unit || '') + '</b>. ' + (q.ex || ''));
|
||||||
|
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
|
||||||
|
doRender(fb);
|
||||||
|
} else { fb.className = 'feedback'; var nb2 = document.getElementById('nextBtn' + sec); if (nb2) nb2.style.display = 'none'; }
|
||||||
|
updateScoreBar(sec); renderNav(sec); doRender(area);
|
||||||
|
if (!done && !isMcq) {
|
||||||
|
var inp = document.getElementById('ainp' + sec);
|
||||||
|
// preventScroll: иначе фокус прокручивает страницу к блоку задач (внизу §)
|
||||||
|
setTimeout(function () { if (inp) { try { inp.focus({ preventScroll: true }); } catch (e) { inp.focus(); } } }, 80);
|
||||||
|
if (inp) inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') checkNum(sec); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMcq(sec, i) {
|
||||||
|
var s = SEC[sec]; if (!s || s.answered) return;
|
||||||
|
var q = POOLS()[sec][s.idx], ok = i === q.a;
|
||||||
|
s.results[s.idx] = ok; s.selections[s.idx] = i; s.answered = true;
|
||||||
|
if (ok) maybeAwardTask(sec);
|
||||||
|
q.opts.forEach(function (_, j) {
|
||||||
|
var btn = document.getElementById('mcqOpt' + sec + '_' + j); if (!btn) return;
|
||||||
|
btn.disabled = true; if (j === q.a) btn.classList.add('mcq-cor'); else if (j === i && !ok) btn.classList.add('mcq-wrong');
|
||||||
|
});
|
||||||
|
var fb = document.getElementById('fb' + sec);
|
||||||
|
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
|
||||||
|
fb.innerHTML = ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.opts[q.a] + '</b>. ' + (q.ex || '');
|
||||||
|
doRender(fb);
|
||||||
|
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
|
||||||
|
updateScoreBar(sec); renderNav(sec); finishCheck(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNum(sec) {
|
||||||
|
var s = SEC[sec]; if (!s || s.answered) return;
|
||||||
|
var q = POOLS()[sec][s.idx], inp = document.getElementById('ainp' + sec), fb = document.getElementById('fb' + sec);
|
||||||
|
var val = (inp.value || '').trim().replace(',', '.'), num = parseFloat(val);
|
||||||
|
if (!val || isNaN(num)) { fb.className = 'feedback show fb-fail'; fb.innerHTML = 'Введите числовой ответ!'; return; }
|
||||||
|
s.answered = true;
|
||||||
|
var tol = q.tol !== undefined ? q.tol : 0.03;
|
||||||
|
var ok = q.a === 0 ? Math.abs(num) < 0.05 : Math.abs((num - q.a) / q.a) < tol;
|
||||||
|
s.results[s.idx] = ok; if (ok) maybeAwardTask(sec);
|
||||||
|
inp.disabled = true; inp.style.borderColor = ok ? 'var(--ok)' : 'var(--fail)';
|
||||||
|
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
|
||||||
|
fb.innerHTML = ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.a + ' ' + (q.unit || '') + '</b>. ' + (q.ex || '');
|
||||||
|
doRender(fb);
|
||||||
|
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
|
||||||
|
updateScoreBar(sec); renderNav(sec); finishCheck(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAwardTask(sec) {
|
||||||
|
var s = SEC[sec]; if (s._awarded === undefined) s._awarded = {};
|
||||||
|
if (s._awarded[s.idx]) return; s._awarded[s.idx] = 1; addXp(5, sec + '-task');
|
||||||
|
}
|
||||||
|
function finishCheck(sec) {
|
||||||
|
var s = SEC[sec];
|
||||||
|
if (s.results.every(function (r) { return r !== null; })) setTimeout(function () { showSummary(sec); }, 1600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTask(sec) {
|
||||||
|
var s = SEC[sec], pool = POOLS()[sec];
|
||||||
|
var next = -1;
|
||||||
|
for (var k = 1; k <= pool.length; k++) { var j = (s.idx + k) % pool.length; if (s.results[j] === null) { next = j; break; } }
|
||||||
|
if (next === -1) { showSummary(sec); return; }
|
||||||
|
s.idx = next; s.answered = s.results[next] !== null; renderTask(sec);
|
||||||
|
}
|
||||||
|
function goToTask(sec, idx) { var s = SEC[sec]; s.idx = idx; s.answered = s.results[idx] !== null; renderTask(sec); }
|
||||||
|
function resetTasks(sec) {
|
||||||
|
var pool = POOLS()[sec];
|
||||||
|
SEC[sec] = { idx: 0, results: pool.map(function () { return null; }), selections: pool.map(function () { return null; }), answered: false, _awarded: {} };
|
||||||
|
var sum = document.getElementById('sum' + sec); if (sum) sum.classList.remove('show');
|
||||||
|
renderTask(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNav(sec) {
|
||||||
|
var s = SEC[sec], pool = POOLS()[sec], nd = document.getElementById('navDots' + sec); if (!nd) return;
|
||||||
|
nd.innerHTML = pool.map(function (_, i) {
|
||||||
|
var cls = 'nav-dot'; if (i === s.idx) cls += ' nd-cur'; if (s.results[i] === true) cls += ' nd-ok'; else if (s.results[i] === false) cls += ' nd-fail';
|
||||||
|
return '<button class="' + cls + '" onclick="goToTask(\'' + sec + '\',' + i + ')">' + (i + 1) + '</button>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
function updateScoreBar(sec) {
|
||||||
|
var s = SEC[sec], pool = POOLS()[sec];
|
||||||
|
var ok = s.results.filter(function (r) { return r === true; }).length;
|
||||||
|
var ans = s.results.filter(function (r) { return r !== null; }).length;
|
||||||
|
setTxt('ok' + sec, ok); setTxt('cur' + sec, ans); setTxt('max' + sec, pool.length);
|
||||||
|
var pf = document.getElementById('prog' + sec); if (pf) pf.style.width = Math.round(ans / pool.length * 100) + '%';
|
||||||
|
}
|
||||||
|
function showSummary(sec) {
|
||||||
|
var s = SEC[sec], pool = POOLS()[sec], sum = document.getElementById('sum' + sec); if (!sum) return;
|
||||||
|
var ok = s.results.filter(function (r) { return r === true; }).length;
|
||||||
|
setTxt('sumScore' + sec, ok + ' / ' + pool.length);
|
||||||
|
var grade = ok === pool.length ? 'Отлично! Все задачи решены.' : ok >= pool.length * 0.6 ? 'Хорошо! Можно повторить ошибки.' : 'Стоит повторить параграф.';
|
||||||
|
setTxt('sumGrade' + sec, grade);
|
||||||
|
sum.classList.add('show');
|
||||||
|
if (ok === pool.length) { bumpProgress(sec, 60); var aId = sec + '_tasks'; if (ACHL()[aId]) achievement(aId); }
|
||||||
|
}
|
||||||
|
function setTxt(id, v) { var e = document.getElementById(id); if (e) e.textContent = v; }
|
||||||
|
|
||||||
|
/* ── тема ──────────────────────────────────────────────────────── */
|
||||||
|
function initTheme() {
|
||||||
|
var t = localStorage.getItem(K.theme) || localStorage.getItem('theme') || 'light';
|
||||||
|
if (t === 'dark') document.documentElement.classList.add('dark');
|
||||||
|
var lab = document.getElementById('theme-lab'); if (lab) lab.textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||||
|
var btn = document.getElementById('theme-btn'); if (!btn) return;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
var d = document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem(K.theme, d ? 'dark' : 'light'); localStorage.setItem('theme', d ? 'dark' : 'light');
|
||||||
|
if (lab) lab.textContent = d ? 'Светлая' : 'Тёмная';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── init ──────────────────────────────────────────────────────── */
|
||||||
|
function init() {
|
||||||
|
resolveCfg();
|
||||||
|
loadProgress(); initTheme(); buildParaSelector(); refreshUI();
|
||||||
|
if (ACHL().start) achievement('start');
|
||||||
|
var first = (PARAS()[0] || {}).id; if (first) goTo(first);
|
||||||
|
refreshUI(); loadServerReadState();
|
||||||
|
W.addEventListener('focus', loadServerReadState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* экспорт */
|
||||||
|
W.goTo = goTo; W.ensureBuilt = ensureBuilt;
|
||||||
|
W.checkNum = checkNum; W.selectMcq = selectMcq; W.nextTask = nextTask; W.goToTask = goToTask; W.resetTasks = resetTasks;
|
||||||
|
W.renderTask = renderTask;
|
||||||
|
W.makeCard = makeCard; W.secNav = secNav; W.readButton = readButton; W.wireReadBtn = wireReadBtn;
|
||||||
|
W.addXp = addXp; W.achievement = achievement; W.bumpProgress = bumpProgress; W.chem8RenderMath = renderMath;
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* chem8_glossary.js — глоссарий учебника «Химия 8».
|
||||||
|
* Самодостаточный drop-in: словарь терминов + плавающая кнопка + модалка с поиском
|
||||||
|
* + авто-подсветка терминов в .card-body (tooltip с определением). Стили инжектятся.
|
||||||
|
* Подключается одним тегом <script src="/js/chem8_glossary.js" defer></script>.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
var D = W.document;
|
||||||
|
|
||||||
|
/* словарь: термин → {d: определение, see: [связанные]} */
|
||||||
|
var G = {
|
||||||
|
'атом': { d: 'Мельчайшая химически неделимая частица вещества: ядро (протоны и нейтроны) + электроны.', see: ['химический элемент', 'нуклид'] },
|
||||||
|
'химический элемент': { d: 'Вид атомов с одинаковым зарядом ядра (числом протонов).', see: ['атом'] },
|
||||||
|
'относительная атомная масса': { d: 'Безразмерная величина $A_r$ — во сколько раз масса атома больше 1/12 массы атома углерода-12.', see: ['относительная молекулярная масса'] },
|
||||||
|
'относительная молекулярная масса': { d: 'Сумма относительных атомных масс всех атомов в формуле ($M_r$).', see: ['молярная масса'] },
|
||||||
|
'простое вещество': { d: 'Вещество из атомов одного элемента (O₂, Fe).', see: ['сложное вещество'] },
|
||||||
|
'сложное вещество': { d: 'Вещество из атомов разных элементов (H₂O, CaCO₃).', see: ['простое вещество'] },
|
||||||
|
'химическая формула': { d: 'Запись состава вещества символами элементов с индексами.', see: [] },
|
||||||
|
'химическое количество': { d: 'Физическая величина $n$ (порция вещества), измеряется в молях.', see: ['моль', 'постоянная Авогадро'] },
|
||||||
|
'моль': { d: 'Единица химического количества: содержит $6{,}02\\cdot10^{23}$ частиц (число Авогадро).', see: ['постоянная Авогадро'] },
|
||||||
|
'постоянная Авогадро': { d: '$N_A = 6{,}02\\cdot10^{23}$ частиц/моль — число частиц в 1 моль.', see: ['моль'] },
|
||||||
|
'молярная масса': { d: 'Масса 1 моль вещества $M$ (г/моль); численно равна $M_r$.', see: ['относительная молекулярная масса'] },
|
||||||
|
'молярный объём': { d: 'Объём 1 моль газа; при н.у. $V_m = 22{,}4$ л/моль.', see: [] },
|
||||||
|
'оксид': { d: 'Сложное вещество из элемента и кислорода (с.о. −2): основный, кислотный, амфотерный, несолеобразующий.', see: ['основный оксид', 'кислотный оксид'] },
|
||||||
|
'основный оксид': { d: 'Оксид металла, реагирует с кислотами (CaO, Na₂O).', see: ['оксид'] },
|
||||||
|
'кислотный оксид': { d: 'Оксид неметалла, реагирует со щелочами (CO₂, SO₃).', see: ['оксид'] },
|
||||||
|
'амфотерность': { d: 'Способность вещества проявлять и кислотные, и основные свойства (Zn(OH)₂, Al(OH)₃).', see: ['оксид', 'основание'] },
|
||||||
|
'кислота': { d: 'Вещество с атомами водорода, способными замещаться металлом, и кислотным остатком.', see: ['основность'] },
|
||||||
|
'основность': { d: 'Число атомов водорода в кислоте, способных замещаться металлом.', see: ['кислота'] },
|
||||||
|
'основание': { d: 'Вещество из металла и гидроксогрупп OH; растворимые — щёлочи.', see: ['щёлочь', 'нейтрализация'] },
|
||||||
|
'щёлочь': { d: 'Растворимое в воде основание (NaOH, KOH, Ba(OH)₂).', see: ['основание'] },
|
||||||
|
'соль': { d: 'Вещество из катионов металла и анионов кислотного остатка (NaCl, CaCO₃).', see: ['реакция ионного обмена'] },
|
||||||
|
'нейтрализация': { d: 'Реакция кислоты с основанием: соль + вода.', see: ['кислота', 'основание'] },
|
||||||
|
'индикатор': { d: 'Вещество, меняющее окраску в зависимости от среды (лакмус, фенолфталеин, метилоранж).', see: [] },
|
||||||
|
'реакция ионного обмена': { d: 'Реакция между растворами, идущая до конца при образовании осадка ↓, газа ↑ или воды.', see: ['соль', 'растворимость'] },
|
||||||
|
'ряд активности металлов': { d: 'Ряд металлов по убыванию химической активности; металл вытесняет менее активные.', see: [] },
|
||||||
|
'генетическая связь': { d: 'Связь между классами веществ через цепочки превращений (металл→оксид→основание→соль).', see: [] },
|
||||||
|
'периодический закон': { d: 'Свойства элементов периодически зависят от заряда ядра их атомов (Д. И. Менделеев, 1869).', see: ['периодическая система'] },
|
||||||
|
'периодическая система': { d: 'Таблица элементов: периоды (строки) и группы (столбцы).', see: ['период', 'группа'] },
|
||||||
|
'период': { d: 'Горизонтальный ряд в ПСХЭ; номер = число электронных слоёв.', see: ['периодическая система'] },
|
||||||
|
'группа': { d: 'Вертикальный столбец ПСХЭ; номер = число внешних электронов.', see: ['периодическая система'] },
|
||||||
|
'нуклид': { d: 'Вид атомов с определёнными Z (протоны) и N (нейтроны).', see: ['изотопы', 'массовое число'] },
|
||||||
|
'массовое число': { d: 'Число протонов и нейтронов в ядре: $A = Z + N$.', see: ['нуклид'] },
|
||||||
|
'изотопы': { d: 'Атомы одного элемента с разным числом нейтронов (одинаковый Z, разный A).', see: ['нуклид'] },
|
||||||
|
'электронное облако': { d: 'Область вокруг ядра, где электрон бывает чаще всего.', see: ['орбиталь'] },
|
||||||
|
'орбиталь': { d: 'Форма электронного облака: s — сфера, p — гантель.', see: ['электронное облако'] },
|
||||||
|
'электроотрицательность': { d: 'Способность атома притягивать к себе общие электроны.', see: ['ковалентная связь'] },
|
||||||
|
'ковалентная связь': { d: 'Связь за счёт общих электронных пар (между неметаллами).', see: ['электроотрицательность', 'ионная связь'] },
|
||||||
|
'ионная связь': { d: 'Связь за счёт полной передачи электронов от металла к неметаллу; образуются ионы.', see: ['ковалентная связь'] },
|
||||||
|
'металлическая связь': { d: 'Связь ион-остовов металла «электронным газом» из общих электронов.', see: [] },
|
||||||
|
'кристаллическая решётка': { d: 'Упорядоченное расположение частиц в кристалле: ионная, атомная, молекулярная, металлическая.', see: [] },
|
||||||
|
'степень окисления': { d: 'Условный заряд атома в соединении (H +1, O −2, сумма = 0).', see: ['окисление', 'восстановление'] },
|
||||||
|
'окисление': { d: 'Процесс отдачи электронов (степень окисления повышается).', see: ['восстановление', 'степень окисления'] },
|
||||||
|
'восстановление': { d: 'Процесс приёма электронов (степень окисления понижается).', see: ['окисление'] },
|
||||||
|
'окислитель': { d: 'Частица, принимающая электроны (сама восстанавливается).', see: ['восстановитель'] },
|
||||||
|
'восстановитель': { d: 'Частица, отдающая электроны (сама окисляется).', see: ['окислитель'] },
|
||||||
|
'окислительно-восстановительная реакция': { d: 'Реакция с изменением степеней окисления (переход электронов).', see: ['степень окисления'] },
|
||||||
|
'смесь': { d: 'Несколько веществ вместе: однородная (раствор) или неоднородная.', see: ['раствор'] },
|
||||||
|
'раствор': { d: 'Однородная смесь растворителя и растворённого вещества.', see: ['растворимость', 'массовая доля'] },
|
||||||
|
'растворимость': { d: 'Масса вещества, растворяющаяся в 100 г воды при данной температуре.', see: ['раствор'] },
|
||||||
|
'насыщенный раствор': { d: 'Раствор, в котором вещество больше не растворяется при данной температуре.', see: ['раствор'] },
|
||||||
|
'массовая доля': { d: 'Отношение массы растворённого вещества к массе раствора: $w = m_{в-ва}/m_{р-ра}$.', see: ['раствор'] },
|
||||||
|
'молярная концентрация': { d: 'Химическое количество вещества в 1 л раствора: $c = n/V$ (моль/л).', see: ['раствор'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
var TERMS = Object.keys(G).sort(function (a, b) { return b.length - a.length; }); // длинные раньше
|
||||||
|
|
||||||
|
function injectCSS() {
|
||||||
|
if (D.getElementById('chem8-gloss-css')) return;
|
||||||
|
var s = D.createElement('style'); s.id = 'chem8-gloss-css';
|
||||||
|
s.textContent =
|
||||||
|
'.gloss{border-bottom:1.5px dotted var(--pri,#d97706);cursor:help;text-decoration:none}'
|
||||||
|
+ '.gl-fab{position:fixed;left:16px;bottom:16px;z-index:55;display:inline-flex;align-items:center;gap:7px;padding:9px 14px;border:none;border-radius:99px;background:var(--pri,#d97706);color:#fff;font-weight:700;font-size:.84rem;cursor:pointer;box-shadow:0 6px 18px rgba(0,0,0,.18);font-family:inherit}'
|
||||||
|
+ '.gl-fab:hover{filter:brightness(1.08)}.gl-fab svg{width:16px;height:16px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}'
|
||||||
|
+ '.gl-modal{position:fixed;inset:0;z-index:80;background:rgba(0,0,0,.45);display:none;align-items:flex-start;justify-content:center;padding:40px 16px;overflow:auto}'
|
||||||
|
+ '.gl-modal.show{display:flex}'
|
||||||
|
+ '.gl-box{background:var(--card,#fff);color:var(--text,#1c1917);border-radius:16px;max-width:600px;width:100%;padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.3)}'
|
||||||
|
+ '.gl-h{display:flex;align-items:center;gap:10px;margin-bottom:12px}.gl-h h3{font-family:Outfit,sans-serif;font-size:1.15rem;font-weight:800;flex:1}'
|
||||||
|
+ '.gl-close{border:none;background:transparent;font-size:1.4rem;cursor:pointer;color:var(--muted,#888);line-height:1}'
|
||||||
|
+ '.gl-search{width:100%;padding:10px 13px;border:1.5px solid var(--border,#ddd);border-radius:10px;background:var(--card,#fff);color:var(--text,#1c1917);font-family:inherit;font-size:.95rem;margin-bottom:12px}'
|
||||||
|
+ '.gl-list{max-height:60vh;overflow:auto}'
|
||||||
|
+ '.gl-item{padding:10px 12px;border-bottom:1px solid var(--border,#eee)}.gl-item:last-child{border-bottom:0}'
|
||||||
|
+ '.gl-term{font-weight:800;color:var(--pri-d,#b45309);text-transform:capitalize}'
|
||||||
|
+ '.gl-def{font-size:.9rem;margin-top:3px;line-height:1.5}'
|
||||||
|
+ '.gl-see{font-size:.8rem;color:var(--muted,#888);margin-top:4px}'
|
||||||
|
+ '.gl-pop{position:absolute;z-index:90;max-width:280px;background:var(--card,#fff);color:var(--text,#1c1917);border:1.5px solid var(--pri,#d97706);border-radius:10px;padding:10px 13px;font-size:.86rem;line-height:1.5;box-shadow:0 8px 24px rgba(0,0,0,.2);display:none}'
|
||||||
|
+ '.gl-pop.show{display:block}.gl-pop b{color:var(--pri-d,#b45309);text-transform:capitalize}';
|
||||||
|
D.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
||||||
|
|
||||||
|
/* авто-подсветка терминов в .card-body (первое вхождение каждого, в текстовых узлах) */
|
||||||
|
function decorate(root) {
|
||||||
|
if (!root) return;
|
||||||
|
var bodies = root.matches && root.matches('.card-body') ? [root] : root.querySelectorAll ? root.querySelectorAll('.card-body') : [];
|
||||||
|
Array.prototype.forEach.call(bodies, function (body) {
|
||||||
|
if (body._glossed) return; body._glossed = 1;
|
||||||
|
var used = {};
|
||||||
|
TERMS.forEach(function (term) {
|
||||||
|
if (used[term]) return;
|
||||||
|
var walker = D.createTreeWalker(body, W.NodeFilter.SHOW_TEXT, null);
|
||||||
|
var node, re = new RegExp('(^|[^а-яёА-ЯЁ-])(' + esc(term) + ')(?![а-яёА-ЯЁ])', 'i');
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
if (node.parentNode && (node.parentNode.classList && (node.parentNode.classList.contains('gloss') || node.parentNode.closest('.gloss,abbr,a,.ph-formula,.main-f,code')))) continue;
|
||||||
|
var m = node.nodeValue.match(re);
|
||||||
|
if (m) {
|
||||||
|
var idx = m.index + m[1].length;
|
||||||
|
var before = node.nodeValue.slice(0, idx), word = node.nodeValue.slice(idx, idx + term.length), after = node.nodeValue.slice(idx + term.length);
|
||||||
|
var ab = D.createElement('abbr'); ab.className = 'gloss'; ab.setAttribute('data-term', term.toLowerCase()); ab.textContent = word;
|
||||||
|
var frag = D.createDocumentFragment();
|
||||||
|
frag.appendChild(D.createTextNode(before)); frag.appendChild(ab); frag.appendChild(D.createTextNode(after));
|
||||||
|
node.parentNode.replaceChild(frag, node);
|
||||||
|
used[term] = 1; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* popover при наведении/клике на .gloss */
|
||||||
|
var pop;
|
||||||
|
function showPop(ab) {
|
||||||
|
var term = ab.getAttribute('data-term'); var g = G[term]; if (!g) return;
|
||||||
|
if (!pop) { pop = D.createElement('div'); pop.className = 'gl-pop'; D.body.appendChild(pop); }
|
||||||
|
pop.innerHTML = '<b>' + term + '</b><br>' + g.d + (g.see && g.see.length ? '<div class="gl-see">См.: ' + g.see.join(', ') + '</div>' : '');
|
||||||
|
var r = ab.getBoundingClientRect();
|
||||||
|
pop.style.left = Math.min(r.left, W.innerWidth - 300) + 'px';
|
||||||
|
pop.style.top = (r.bottom + W.scrollY + 6) + 'px';
|
||||||
|
pop.classList.add('show');
|
||||||
|
renderMath(pop);
|
||||||
|
}
|
||||||
|
function hidePop() { if (pop) pop.classList.remove('show'); }
|
||||||
|
function renderMath(el) { if (typeof W.renderMathInElement === 'function') { try { W.renderMathInElement(el, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {} } }
|
||||||
|
|
||||||
|
/* модалка */
|
||||||
|
var modal;
|
||||||
|
function buildModal() {
|
||||||
|
modal = D.createElement('div'); modal.className = 'gl-modal';
|
||||||
|
modal.innerHTML = '<div class="gl-box"><div class="gl-h"><h3>Глоссарий — Химия 8</h3><button class="gl-close" aria-label="Закрыть">×</button></div>'
|
||||||
|
+ '<input class="gl-search" placeholder="Поиск термина...">'
|
||||||
|
+ '<div class="gl-list"></div></div>';
|
||||||
|
D.body.appendChild(modal);
|
||||||
|
var list = modal.querySelector('.gl-list'), search = modal.querySelector('.gl-search');
|
||||||
|
function render(q) {
|
||||||
|
q = (q || '').toLowerCase().trim();
|
||||||
|
var keys = Object.keys(G).sort();
|
||||||
|
list.innerHTML = keys.filter(function (t) { return !q || t.indexOf(q) >= 0 || G[t].d.toLowerCase().indexOf(q) >= 0; })
|
||||||
|
.map(function (t) { return '<div class="gl-item"><div class="gl-term">' + t + '</div><div class="gl-def">' + G[t].d + '</div>' + (G[t].see && G[t].see.length ? '<div class="gl-see">См.: ' + G[t].see.join(', ') + '</div>' : '') + '</div>'; }).join('') || '<div class="gl-item">Ничего не найдено.</div>';
|
||||||
|
renderMath(list);
|
||||||
|
}
|
||||||
|
search.addEventListener('input', function () { render(search.value); });
|
||||||
|
modal.querySelector('.gl-close').addEventListener('click', close);
|
||||||
|
modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
|
||||||
|
render('');
|
||||||
|
}
|
||||||
|
function open() { if (!modal) buildModal(); modal.classList.add('show'); var s = modal.querySelector('.gl-search'); if (s) setTimeout(function () { s.focus(); }, 50); }
|
||||||
|
function close() { if (modal) modal.classList.remove('show'); }
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
injectCSS();
|
||||||
|
var fab = D.createElement('button'); fab.className = 'gl-fab';
|
||||||
|
fab.innerHTML = '<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> Глоссарий';
|
||||||
|
fab.addEventListener('click', open);
|
||||||
|
D.body.appendChild(fab);
|
||||||
|
D.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||||
|
// авто-подсветка терминов: при наведении/клике — popover
|
||||||
|
D.body.addEventListener('mouseover', function (e) { if (e.target.classList && e.target.classList.contains('gloss')) showPop(e.target); });
|
||||||
|
D.body.addEventListener('mouseout', function (e) { if (e.target.classList && e.target.classList.contains('gloss')) hidePop(); });
|
||||||
|
D.body.addEventListener('click', function (e) { if (e.target.classList && e.target.classList.contains('gloss')) { e.preventDefault(); showPop(e.target); } });
|
||||||
|
// первичная декорация + наблюдение за лениво строящимися §
|
||||||
|
decorate(D.body);
|
||||||
|
try {
|
||||||
|
var obs = new W.MutationObserver(function (muts) {
|
||||||
|
muts.forEach(function (m) { Array.prototype.forEach.call(m.addedNodes, function (n) { if (n.nodeType === 1) decorate(n); }); });
|
||||||
|
});
|
||||||
|
obs.observe(D.body, { childList: true, subtree: true });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
W.Chem8Glossary = { open: open, decorate: decorate, terms: G };
|
||||||
|
if (D.readyState === 'loading') D.addEventListener('DOMContentLoaded', init); else init();
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/* chem8_intro_widgets.js — виджеты вводного раздела «Химия 8».
|
||||||
|
* Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id].
|
||||||
|
* Используют window.Chem8 (chem8_svg.js): molarMass, elementCounts, arOf, fmt,
|
||||||
|
* moleTriangle, equationBalancer.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
function C() { return W.Chem8 || {}; }
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
function rr(v, d) { var p = Math.pow(10, d == null ? 3 : d); return (Math.round(v * p) / p).toString().replace('.', ','); }
|
||||||
|
|
||||||
|
/* §1 — карта элементов */
|
||||||
|
var EL = {
|
||||||
|
H: [1, 'Водород'], He: [2, 'Гелий'], Li: [3, 'Литий'], Be: [4, 'Бериллий'], B: [5, 'Бор'], C: [6, 'Углерод'],
|
||||||
|
N: [7, 'Азот'], O: [8, 'Кислород'], F: [9, 'Фтор'], Ne: [10, 'Неон'], Na: [11, 'Натрий'], Mg: [12, 'Магний'],
|
||||||
|
Al: [13, 'Алюминий'], Si: [14, 'Кремний'], P: [15, 'Фосфор'], S: [16, 'Сера'], Cl: [17, 'Хлор'], Ar: [18, 'Аргон'],
|
||||||
|
K: [19, 'Калий'], Ca: [20, 'Кальций'], Fe: [26, 'Железо'], Cu: [29, 'Медь'], Zn: [30, 'Цинк'], Ag: [47, 'Серебро'], Ba: [56, 'Барий']
|
||||||
|
};
|
||||||
|
function mount_p1() {
|
||||||
|
var grid = $('p1-el'), info = $('p1-elinfo'); if (!grid || grid._built) return; grid._built = 1;
|
||||||
|
Object.keys(EL).forEach(function (s) {
|
||||||
|
var ar = C().arOf ? C().arOf(s) : '';
|
||||||
|
var c = document.createElement('div'); c.className = 'el-cell';
|
||||||
|
c.innerHTML = '<span class="z">' + EL[s][0] + '</span><span class="s">' + s + '</span><span class="a">' + ar + '</span>';
|
||||||
|
c.addEventListener('click', function () {
|
||||||
|
grid.querySelectorAll('.el-cell').forEach(function (x) { x.classList.remove('on'); }); c.classList.add('on');
|
||||||
|
info.innerHTML = '<b>' + EL[s][1] + '</b> (' + s + ') · порядковый номер Z = ' + EL[s][0] + ' · A_r = ' + ar;
|
||||||
|
});
|
||||||
|
grid.appendChild(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §2 — калькулятор Mr */
|
||||||
|
function mount_p2() {
|
||||||
|
var inp = $('p2-mr-in'), out = $('p2-mr-out'), go = $('p2-mr-go'); if (!inp || inp._built) return; inp._built = 1;
|
||||||
|
function calc() {
|
||||||
|
var f = inp.value.trim(), cnt = C().elementCounts ? C().elementCounts(f) : null, mr = C().molarMass ? C().molarMass(f) : NaN;
|
||||||
|
if (!cnt || isNaN(mr)) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу. Проверьте символы элементов.'; return; }
|
||||||
|
out.className = 'out ok';
|
||||||
|
out.innerHTML = '<b>M_r(' + f + ') = ' + C().fmt(mr) + '</b><br><span class="bd">' +
|
||||||
|
Object.keys(cnt).map(function (e) { return e + ': A_r=' + (C().arOf ? C().arOf(e) : '?') + ' × ' + cnt[e]; }).join(' | ') +
|
||||||
|
'<br>Σ = ' + Object.keys(cnt).map(function (e) { return (C().arOf ? C().arOf(e) : '?') + '·' + cnt[e]; }).join(' + ') + ' = ' + C().fmt(mr) + '</span>';
|
||||||
|
}
|
||||||
|
go.addEventListener('click', calc);
|
||||||
|
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
|
||||||
|
document.querySelectorAll('.p2-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
|
||||||
|
calc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §3 — порция вещества */
|
||||||
|
function mount_p3() {
|
||||||
|
var sub = $('p3-sub'), rng = $('p3-n'), nv = $('p3-nv'), out = $('p3-out'); if (!sub || sub._built) return; sub._built = 1;
|
||||||
|
var M = { H2O: 18, O2: 32, CO2: 44, NaCl: 58.5 };
|
||||||
|
function upd() {
|
||||||
|
var n = parseFloat(rng.value), s = sub.value, m = n * M[s], N = n * 6.02;
|
||||||
|
nv.textContent = n.toFixed(1).replace('.', ',');
|
||||||
|
out.innerHTML = '<span class="bd">n = ' + n.toFixed(1).replace('.', ',') + ' моль<br>m = n·M = ' + n.toFixed(1).replace('.', ',') + ' · ' + String(M[s]).replace('.', ',') + ' = <b>' + rr(m, 1) + ' г</b><br>N = n·N_A = <b>' + rr(N, 2) + '·10²³ частиц</b></span>';
|
||||||
|
}
|
||||||
|
sub.addEventListener('change', upd); rng.addEventListener('input', upd); upd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §4 — счётчик частиц */
|
||||||
|
function mount_p4() {
|
||||||
|
var rng = $('p4-n'), nv = $('p4-nv'), out = $('p4-out'); if (!rng || rng._built) return; rng._built = 1;
|
||||||
|
function upd() { var n = parseFloat(rng.value), N = n * 6.02; nv.textContent = n.toFixed(2).replace('.', ',');
|
||||||
|
out.innerHTML = '<span class="bd">N = n · N_A = ' + n.toFixed(2).replace('.', ',') + ' · 6,02·10²³ = <b>' + rr(N, 2) + '·10²³ частиц</b></span>'; }
|
||||||
|
rng.addEventListener('input', upd); upd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5 — M + объём газа */
|
||||||
|
function mount_p5() {
|
||||||
|
var inp = $('p5-in'), out = $('p5-out'), go = $('p5-go'); if (!inp || inp._built) return; inp._built = 1;
|
||||||
|
function calc() {
|
||||||
|
var f = inp.value.trim(), mr = C().molarMass ? C().molarMass(f) : NaN;
|
||||||
|
if (isNaN(mr)) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу.'; return; }
|
||||||
|
out.className = 'out ok';
|
||||||
|
out.innerHTML = '<span class="bd">M(' + f + ') = <b>' + C().fmt(mr) + ' г/моль</b><br>1 моль газа при н.у. → <b>22,4 л</b><br>Плотность газа ≈ M/22,4 = ' + rr(mr / 22.4) + ' г/л</span>';
|
||||||
|
}
|
||||||
|
go.addEventListener('click', calc); inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); }); calc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §6 / ПР1 — треугольник n–m–M (флагман) */
|
||||||
|
function mount_triangle(mountId, subId) {
|
||||||
|
var mount = $(mountId), sub = $(subId); if (!mount || mount._built || !C().moleTriangle) return; mount._built = 1;
|
||||||
|
var api = C().moleTriangle(mount, {});
|
||||||
|
if (sub) sub.addEventListener('change', function () {
|
||||||
|
var f = sub.value; if (!f) return; var m = C().molarMass(f);
|
||||||
|
if (!isNaN(m) && api && api.set) api.set('M', m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function mount_p6() { mount_triangle('p6-mount', 'p6-sub'); }
|
||||||
|
function mount_pr1() { mount_triangle('pr1-mount', 'pr1-sub'); }
|
||||||
|
|
||||||
|
/* §7 — универсальный калькулятор газа (флагман) */
|
||||||
|
function mount_p7() {
|
||||||
|
var sub = $('p7-sub'), key = $('p7-key'), val = $('p7-val'), go = $('p7-go'), out = $('p7-out'); if (!sub || sub._built) return; sub._built = 1;
|
||||||
|
var Vm = 22.4, NA = 6.02;
|
||||||
|
function calc() {
|
||||||
|
var f = sub.value, M = C().molarMass(f), k = key.value, x = parseFloat((val.value || '').replace(',', '.'));
|
||||||
|
if (isNaN(x)) { out.className = 'out bad'; out.textContent = 'Введите число.'; return; }
|
||||||
|
var n; if (k === 'n') n = x; else if (k === 'm') n = x / M; else if (k === 'V') n = x / Vm; else n = x / NA;
|
||||||
|
var m = n * M, V = n * Vm, N = n * NA;
|
||||||
|
out.className = 'out ok';
|
||||||
|
out.innerHTML = '<span class="bd">M(' + f + ')=' + M + ' г/моль<br>n = <b>' + rr(n) + ' моль</b><br>m = <b>' + rr(m) + ' г</b><br>V(н.у.) = <b>' + rr(V) + ' л</b><br>N = <b>' + rr(N) + '·10²³ частиц</b></span>';
|
||||||
|
}
|
||||||
|
go.addEventListener('click', calc); val.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); }); calc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §8 — балансировщик (флагман) */
|
||||||
|
function mount_p8() {
|
||||||
|
var pick = $('p8-pick'), mount = $('p8-mount'); if (!pick || pick._built || !C().equationBalancer) return; pick._built = 1;
|
||||||
|
function build() { var parts = pick.value.split('|'); C().equationBalancer(mount, { skeleton: parts[0], solution: parts[1].split(',').map(Number) }); }
|
||||||
|
pick.addEventListener('change', build); build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §9 — пошаговый решатель (флагман) */
|
||||||
|
var ST = [
|
||||||
|
{ eq: '2H₂ + O₂ → 2H₂O', given: 'Дано: m(H₂) = 4 г. Найти m(H₂O).',
|
||||||
|
steps: ['M(H₂)=2 г/моль, M(H₂O)=18 г/моль.', 'n(H₂) = m/M = 4/2 = 2 моль.', 'По уравнению n(H₂):n(H₂O) = 2:2 = 1:1 → n(H₂O)=2 моль.', 'm(H₂O) = n·M = 2·18 = 36 г. Ответ: 36 г.'] },
|
||||||
|
{ eq: 'CaCO₃ → CaO + CO₂↑', given: 'Дано: m(CaCO₃) = 100 г. Найти V(CO₂) при н.у.',
|
||||||
|
steps: ['M(CaCO₃)=100 г/моль.', 'n(CaCO₃) = 100/100 = 1 моль.', 'n(CaCO₃):n(CO₂) = 1:1 → n(CO₂)=1 моль.', 'V(CO₂) = n·Vm = 1·22,4 = 22,4 л. Ответ: 22,4 л.'] },
|
||||||
|
{ eq: 'Zn + 2HCl → ZnCl₂ + H₂↑', given: 'Дано: n(Zn) = 0,5 моль. Найти V(H₂) при н.у.',
|
||||||
|
steps: ['n(Zn):n(H₂) = 1:1 → n(H₂)=0,5 моль.', 'V(H₂) = n·Vm = 0,5·22,4 = 11,2 л. Ответ: 11,2 л.'] }
|
||||||
|
];
|
||||||
|
function mount_p9() {
|
||||||
|
var pick = $('p9-pick'), out = $('p9-out'), bStep = $('p9-step'), bAll = $('p9-all'); if (!pick || pick._built) return; pick._built = 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 = '<b>' + p.eq + '</b><br><span style="color:var(--muted)">' + p.given + '</span><div style="margin-top:8px">';
|
||||||
|
for (var i = 0; i < shown; i++) html += '<div class="def-box" style="margin:6px 0">' + p.steps[i] + '</div>';
|
||||||
|
if (shown === 0) html += '<span style="color:var(--muted)">Нажмите «Следующий шаг», чтобы решать пошагово.</span>';
|
||||||
|
html += '</div>'; out.className = shown >= p.steps.length ? 'out ok' : 'out'; out.innerHTML = html;
|
||||||
|
if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch (e) {}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
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 = { final1: mount_final1, p6: mount_p6, p7: mount_p7, p8: mount_p8, p9: mount_p9 };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/* chem8_mol.js — 3D-модели молекул и кристаллических решёток (U4).
|
||||||
|
* Поверх biochem-core (window.BIO): vsepr + render3D. Вращение мышью/пальцем
|
||||||
|
* (window-listeners, без setPointerCapture). Экспорт: window.Chem8Mol.
|
||||||
|
*/
|
||||||
|
(function (W) {
|
||||||
|
'use strict';
|
||||||
|
var D = W.document;
|
||||||
|
function BIO() { return W.BIO; }
|
||||||
|
function C() { return W.Chem8 || {}; }
|
||||||
|
|
||||||
|
/* предопределённые молекулы: atoms + bonds */
|
||||||
|
var MOL = {
|
||||||
|
H2: { atoms: [{ id: 1, s: 'H' }, { id: 2, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Водород H₂' },
|
||||||
|
Cl2: { atoms: [{ id: 1, s: 'Cl' }, { id: 2, s: 'Cl' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Хлор Cl₂' },
|
||||||
|
O2: { atoms: [{ id: 1, s: 'O' }, { id: 2, s: 'O' }], bonds: [{ f: 1, t: 2, o: 2 }], name: 'Кислород O₂' },
|
||||||
|
N2: { atoms: [{ id: 1, s: 'N' }, { id: 2, s: 'N' }], bonds: [{ f: 1, t: 2, o: 3 }], name: 'Азот N₂' },
|
||||||
|
HCl: { atoms: [{ id: 1, s: 'H' }, { id: 2, s: 'Cl' }], bonds: [{ f: 1, t: 2, o: 1 }], name: 'Хлороводород HCl' },
|
||||||
|
H2O: { atoms: [{ id: 1, s: 'O' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }], name: 'Вода H₂O' },
|
||||||
|
CO2: { atoms: [{ id: 1, s: 'C' }, { id: 2, s: 'O' }, { id: 3, s: 'O' }], bonds: [{ f: 1, t: 2, o: 2 }, { f: 1, t: 3, o: 2 }], name: 'Углекислый газ CO₂' },
|
||||||
|
NH3: { atoms: [{ id: 1, s: 'N' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }, { id: 4, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }, { f: 1, t: 4, o: 1 }], name: 'Аммиак NH₃' },
|
||||||
|
CH4: { atoms: [{ id: 1, s: 'C' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }, { id: 4, s: 'H' }, { id: 5, s: 'H' }], bonds: [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }, { f: 1, t: 4, o: 1 }, { f: 1, t: 5, o: 1 }], name: 'Метан CH₄' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function mkCanvas(host, h) {
|
||||||
|
var cv = D.createElement('canvas'); cv.className = 'mol-cv';
|
||||||
|
cv.style.width = '100%'; cv.style.height = (h || 200) + 'px'; cv.style.touchAction = 'none';
|
||||||
|
cv.style.borderRadius = '12px'; cv.style.display = 'block';
|
||||||
|
host.appendChild(cv); return cv;
|
||||||
|
}
|
||||||
|
function fit(cv) {
|
||||||
|
var dpr = W.devicePixelRatio || 1, w = cv.offsetWidth || 280, h = cv.offsetHeight || 200;
|
||||||
|
cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
|
||||||
|
var ctx = cv.getContext && cv.getContext('2d'); if (!ctx) return null; // jsdom без canvas
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
return { ctx: ctx, W: w, H: h };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* общий движок вращения: state выше redraw, window-listeners */
|
||||||
|
function attachRotate(cv, state, redraw) {
|
||||||
|
var dragging = false, lx = 0, ly = 0;
|
||||||
|
cv.addEventListener('pointerdown', function (e) { dragging = true; lx = e.clientX; ly = e.clientY; state.spin = false; });
|
||||||
|
W.addEventListener('pointermove', function (e) {
|
||||||
|
if (!dragging) return;
|
||||||
|
state.rotY += (e.clientX - lx) * 0.01; state.rotX += (e.clientY - ly) * 0.01;
|
||||||
|
lx = e.clientX; ly = e.clientY; redraw();
|
||||||
|
});
|
||||||
|
W.addEventListener('pointerup', function () { dragging = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 3D-модель молекулы ── */
|
||||||
|
function molModel(mount, key) {
|
||||||
|
var host = typeof mount === 'string' ? D.querySelector(mount) : mount;
|
||||||
|
if (!host || !BIO()) return null;
|
||||||
|
var keys = Object.keys(MOL);
|
||||||
|
host.innerHTML = '<div class="fld"><label>Молекула</label><select class="mol-sel">' +
|
||||||
|
keys.map(function (k) { return '<option value="' + k + '"' + (k === key ? ' selected' : '') + '>' + MOL[k].name + '</option>'; }).join('') + '</select>'
|
||||||
|
+ '<button class="btn mol-spin">⟳ Вращение</button></div>';
|
||||||
|
var stage = D.createElement('div'); host.appendChild(stage);
|
||||||
|
var cv = mkCanvas(stage, 200);
|
||||||
|
var info = D.createElement('div'); info.className = 'out mol-info'; host.appendChild(info);
|
||||||
|
var sel = host.querySelector('.mol-sel'), spinBtn = host.querySelector('.mol-spin');
|
||||||
|
var state = { rotX: -0.35, rotY: 0.6, scale: 2.6, spin: true };
|
||||||
|
var cur;
|
||||||
|
function load(k) {
|
||||||
|
cur = MOL[k]; var g = BIO().vsepr(cur.atoms, cur.bonds); cur.g = g;
|
||||||
|
var pol = BIO().polarity(cur.atoms, cur.bonds);
|
||||||
|
var mr = C().molarMass ? C().molarMass(k) : BIO().molarMass(cur.atoms);
|
||||||
|
var bondTxt = cur.atoms.length === 2 && C().bondClass
|
||||||
|
? C().bondClass(cur.atoms[0].s, cur.atoms[1].s).type
|
||||||
|
: (pol.label === 'Ионная' ? 'ионная' : 'ковалентная');
|
||||||
|
info.className = 'out mol-info ok';
|
||||||
|
info.innerHTML = '<span class="bd"><b>' + cur.name + '</b> · M = ' + (C().fmt ? C().fmt(mr) : mr) + ' г/моль<br>'
|
||||||
|
+ 'Связь: ' + bondTxt + ' · молекула: <b>' + pol.label.toLowerCase() + '</b>'
|
||||||
|
+ (g.shape ? ' · форма: ' + g.shape : '') + '</span>';
|
||||||
|
}
|
||||||
|
function redraw() {
|
||||||
|
var d = fit(cv); if (!d) return;
|
||||||
|
BIO().render3D(d.ctx, cur.g.atoms3d, cur.bonds, { W: d.W, H: d.H, rotX: state.rotX, rotY: state.rotY, scale: state.scale }, { bg: '#0b1220' });
|
||||||
|
}
|
||||||
|
sel.addEventListener('change', function () { load(sel.value); redraw(); });
|
||||||
|
spinBtn.addEventListener('click', function () { state.spin = !state.spin; spinBtn.classList.toggle('primary', state.spin); });
|
||||||
|
attachRotate(cv, state, redraw);
|
||||||
|
load(key && MOL[key] ? key : keys[0]);
|
||||||
|
redraw();
|
||||||
|
if (fit(cv)) (function loop() { if (state.spin) { state.rotY += 0.012; redraw(); } W.requestAnimationFrame(loop); })(); // не стартуем цикл без canvas-контекста (jsdom)
|
||||||
|
return { el: host };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── кристаллические решётки (§41) ── */
|
||||||
|
var LAT = {
|
||||||
|
ionic: { name: 'Ионная (NaCl)', build: function () { return cube(['Na', 'Cl']); }, note: 'Узлы — ионы Na⁺ и Cl⁻. Прочная решётка → тугоплавкие, твёрдые вещества.' },
|
||||||
|
atomic: { name: 'Атомная (алмаз)', build: function () { return cube(['C', 'C']); }, note: 'Узлы — атомы, связанные ковалентно. Очень твёрдые, тугоплавкие.' },
|
||||||
|
molecular: { name: 'Молекулярная (лёд)', build: function () { return cube(['O', 'O']); }, note: 'Узлы — молекулы со слабым притяжением. Летучие, легкоплавкие.' },
|
||||||
|
metallic: { name: 'Металлическая (Fe)', build: function () { return cube(['Fe', 'Fe'], true); }, note: 'Ион-остовы металла в «электронном газе». Ковкие, проводят ток.' }
|
||||||
|
};
|
||||||
|
function cube(symPair, electrons) {
|
||||||
|
var L = 16, atoms = [], id = 1;
|
||||||
|
for (var xi = -1; xi <= 1; xi += 2) for (var yi = -1; yi <= 1; yi += 2) for (var zi = -1; zi <= 1; zi += 2) {
|
||||||
|
var parity = ((xi + yi + zi) / 2 + 3) % 2;
|
||||||
|
atoms.push({ id: id++, s: symPair[parity], x: xi * L, y: yi * L, z: zi * L });
|
||||||
|
}
|
||||||
|
var bonds = [];
|
||||||
|
for (var i = 0; i < atoms.length; i++) for (var j = i + 1; j < atoms.length; j++) {
|
||||||
|
var a = atoms[i], b = atoms[j], dd = Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.z - b.z);
|
||||||
|
if (dd === 2 * L) bonds.push({ f: a.id, t: b.id, o: 1 });
|
||||||
|
}
|
||||||
|
if (electrons) for (var e = 0; e < 6; e++) atoms.push({ id: id++, s: 'H', x: (e % 3 - 1) * L, y: ((e / 3 | 0) * 2 - 1) * L * 0.5, z: 0 }); // «электроны» как мелкие точки (H — мелкий радиус)
|
||||||
|
return { atoms: atoms, bonds: bonds };
|
||||||
|
}
|
||||||
|
function latticeViewer(mount, type) {
|
||||||
|
var host = typeof mount === 'string' ? D.querySelector(mount) : mount;
|
||||||
|
if (!host || !BIO()) return null;
|
||||||
|
var keys = Object.keys(LAT);
|
||||||
|
host.innerHTML = '<div class="fld"><label>Тип решётки</label><select class="lat-sel">' +
|
||||||
|
keys.map(function (k) { return '<option value="' + k + '"' + (k === type ? ' selected' : '') + '>' + LAT[k].name + '</option>'; }).join('') + '</select></div>';
|
||||||
|
var stage = D.createElement('div'); host.appendChild(stage);
|
||||||
|
var cv = mkCanvas(stage, 200);
|
||||||
|
var info = D.createElement('div'); info.className = 'out'; host.appendChild(info);
|
||||||
|
var sel = host.querySelector('.lat-sel');
|
||||||
|
var state = { rotX: -0.4, rotY: 0.5, scale: 2.4, spin: true };
|
||||||
|
var cur;
|
||||||
|
function load(k) { var l = LAT[k]; cur = l.build(); info.className = 'out ok'; info.innerHTML = '<span class="bd"><b>' + l.name + '</b><br>' + l.note + '</span>'; }
|
||||||
|
function redraw() { var d = fit(cv); if (!d) return; BIO().render3D(d.ctx, cur.atoms, cur.bonds, { W: d.W, H: d.H, rotX: state.rotX, rotY: state.rotY, scale: state.scale }, { bg: '#0b1220' }); }
|
||||||
|
sel.addEventListener('change', function () { load(sel.value); redraw(); });
|
||||||
|
attachRotate(cv, state, redraw);
|
||||||
|
load(type && LAT[type] ? type : keys[0]); redraw();
|
||||||
|
if (fit(cv)) (function loop() { if (state.spin) { state.rotY += 0.01; redraw(); } W.requestAnimationFrame(loop); })();
|
||||||
|
return { el: host };
|
||||||
|
}
|
||||||
|
|
||||||
|
W.Chem8Mol = { molModel: molModel, latticeViewer: latticeViewer, MOL: MOL };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,980 @@
|
|||||||
|
/* chem8_svg.js — химические наглядные примитивы для учебника «Химия 8».
|
||||||
|
*
|
||||||
|
* Неймспейс: window.Chem8.*
|
||||||
|
* Молекулярные модели (структурные / шаростержневые / 3D) — НЕ здесь, а через
|
||||||
|
* biochem-core.js (window.BioChem). Здесь только то, чего там нет: рендер формул и
|
||||||
|
* уравнений, ионы, степени окисления, интерактивные виджеты (растворимость, ряд
|
||||||
|
* активности, индикаторы, классификаторы, калькуляторы расчётов и т. п.).
|
||||||
|
*
|
||||||
|
* Phase 0: реализованы чистые текстовые примитивы (ionLabel, chemEq, formula).
|
||||||
|
* Остальные хелперы — каркасы-заглушки, наполняются по фазам (см. PLAN_CHEMISTRY_8.md, разд. B).
|
||||||
|
*
|
||||||
|
* Правила (CLAUDE.md / план):
|
||||||
|
* - без эмоджи, только inline SVG .ic;
|
||||||
|
* - в KaTeX-шаблонах двойной backslash (\\to, \\downarrow, \\rightleftharpoons);
|
||||||
|
* - drag/слайдеры: window-listeners + state ВЫШЕ redraw(), без setPointerCapture.
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var SUB = { '0':'₀','1':'₁','2':'₂','3':'₃','4':'₄',
|
||||||
|
'5':'₅','6':'₆','7':'₇','8':'₈','9':'₉' };
|
||||||
|
var SUP = { '0':'⁰','1':'¹','2':'²','3':'³','4':'⁴',
|
||||||
|
'5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹',
|
||||||
|
'+':'⁺','-':'⁻' };
|
||||||
|
|
||||||
|
function toSub(digits) {
|
||||||
|
return String(digits).replace(/[0-9]/g, function (d) { return SUB[d]; });
|
||||||
|
}
|
||||||
|
function toSup(s) {
|
||||||
|
return String(s).replace(/[0-9+\-]/g, function (c) { return SUP[c] || c; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* formula('CaCO3') -> 'CaCO₃' : числовые индексы атомов в подстрочные.
|
||||||
|
Не трогает множители-коэффициенты в начале (их рендерит chemEq). */
|
||||||
|
function formula(src) {
|
||||||
|
if (src == null) return '';
|
||||||
|
return String(src).replace(/([A-Za-z\)\]])(\d+)/g, function (_, a, n) {
|
||||||
|
return a + toSub(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ionLabel('SO4', -2) -> 'SO₄²⁻' ; ionLabel('Ca', 2) -> 'Ca²⁺' ; ionLabel('Na', 1) -> 'Na⁺' */
|
||||||
|
function ionLabel(form, charge) {
|
||||||
|
var body = formula(form);
|
||||||
|
var c = Number(charge) || 0;
|
||||||
|
if (c === 0) return body;
|
||||||
|
var mag = Math.abs(c);
|
||||||
|
var sign = c > 0 ? '+' : '-';
|
||||||
|
var num = mag === 1 ? '' : String(mag);
|
||||||
|
return body + toSup(num + sign);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chemEq('2Na + 2H2O -> 2NaOH + H2^', {arrow:'->'}) -> HTML-строка с индексами,
|
||||||
|
стрелками (= → ⇌), значками газа (↑) и осадка (↓), условием над стрелкой.
|
||||||
|
Токены: '->'/'=' необратимая, '<->'/'<=>' обратимая, '^' газ, 'v' осадок.
|
||||||
|
opts.cond — подпись над стрелкой (например 't', 'кат.', 'эл. ток'). */
|
||||||
|
function chemEq(src, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var s = String(src == null ? '' : src).trim();
|
||||||
|
var arrowHtml = ' <span class="ceq-arrow">' + arrowGlyph(s, opts) + condHtml(opts) + '</span> ';
|
||||||
|
// выделяем стрелку
|
||||||
|
var parts = s.split(/<->|<=>|->|⇌|=(?![^(]*\))|→/);
|
||||||
|
var left = parts[0] || '';
|
||||||
|
var right = parts.length > 1 ? parts.slice(1).join(' ') : '';
|
||||||
|
var html = renderSide(left);
|
||||||
|
if (right) html += arrowHtml + renderSide(right);
|
||||||
|
return '<span class="ceq">' + html + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrowGlyph(s, opts) {
|
||||||
|
if (opts.arrow === '<->' || opts.arrow === '<=>' || /<->|<=>|⇌/.test(s)) return '⇌';
|
||||||
|
return '→'; // →
|
||||||
|
}
|
||||||
|
function condHtml(opts) {
|
||||||
|
if (!opts.cond) return '';
|
||||||
|
return '<sup class="ceq-cond">' + escapeHtml(opts.cond) + '</sup>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* одна сторона уравнения: разбор на вещества по '+', значки ↑/↓ */
|
||||||
|
function renderSide(side) {
|
||||||
|
return side.split('+').map(function (term) {
|
||||||
|
var t = term.trim();
|
||||||
|
if (!t) return '';
|
||||||
|
var gas = false, prec = false;
|
||||||
|
t = t.replace(/\^|↑/g, function () { gas = true; return ''; })
|
||||||
|
.replace(/(^|[A-Za-z0-9\)])v(\b|$)|↓/g, function (m) {
|
||||||
|
prec = true; return m.replace(/v|↓/, '');
|
||||||
|
});
|
||||||
|
// коэффициент в начале
|
||||||
|
var coef = '';
|
||||||
|
t = t.replace(/^(\d+)/, function (_, n) { coef = n; return ''; });
|
||||||
|
var out = (coef ? coef : '') + formula(t.trim());
|
||||||
|
if (gas) out += '↑';
|
||||||
|
if (prec) out += '↓';
|
||||||
|
return out;
|
||||||
|
}).filter(Boolean).join(' + ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s).replace(/[&<>"']/g, function (c) {
|
||||||
|
return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Относительные атомные массы 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 =
|
||||||
|
'<div class="mtri">' +
|
||||||
|
'<svg class="mtri-svg" viewBox="0 0 200 150" aria-hidden="true">' +
|
||||||
|
'<polygon points="100,14 18,140 182,140" fill="none" stroke="currentColor" stroke-width="2" opacity=".5"/>' +
|
||||||
|
'<line x1="59" y1="77" x2="141" y2="77" stroke="currentColor" stroke-width="1.5" opacity=".4"/>' +
|
||||||
|
'<text x="100" y="52" text-anchor="middle" font-size="26" font-weight="800" fill="currentColor">m</text>' +
|
||||||
|
'<text x="62" y="124" text-anchor="middle" font-size="22" font-weight="800" fill="currentColor">n</text>' +
|
||||||
|
'<text x="140" y="124" text-anchor="middle" font-size="22" font-weight="800" fill="currentColor">M</text>' +
|
||||||
|
'</svg>' +
|
||||||
|
'<div class="mtri-fields">' +
|
||||||
|
fieldHtml('n', 'n, моль', 'химическое количество') +
|
||||||
|
fieldHtml('m', 'm, г', 'масса вещества') +
|
||||||
|
fieldHtml('M', 'M, г/моль', 'молярная масса') +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="mtri-out" data-out>Введите любые два значения — третье вычислится.</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
function fieldHtml(key, label, hint) {
|
||||||
|
return '<label class="mtri-f"><span class="mtri-lab">' + label + '</span>' +
|
||||||
|
'<input type="text" inputmode="decimal" data-k="' + key + '" placeholder="?" ' +
|
||||||
|
'title="' + hint + '"></label>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<b>' + target + ' = ' + fmt(res) + unit + '</b><span class="mtri-form">' + formula + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
'<div class="ceqb">' +
|
||||||
|
'<div class="ceqb-row" data-eq>' +
|
||||||
|
renderSpecies(left) + '<span class="ceqb-arrow">→</span>' + renderSpecies(right) +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ceqb-actions">' +
|
||||||
|
'<button type="button" class="ceqb-btn primary" data-check>Проверить</button>' +
|
||||||
|
(opts.solution ? '<button type="button" class="ceqb-btn" data-solve>Показать решение</button>' : '') +
|
||||||
|
'<button type="button" class="ceqb-btn" data-reset>Сброс</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ceqb-out" data-out></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
function renderSpecies(list) {
|
||||||
|
return list.map(function (sp, i) {
|
||||||
|
var gi = all.indexOf(sp);
|
||||||
|
return (i ? '<span class="ceqb-plus">+</span>' : '') +
|
||||||
|
'<span class="ceqb-sp"><input type="number" min="1" step="1" class="ceqb-coef" ' +
|
||||||
|
'data-i="' + gi + '" value="1"><span class="ceqb-f">' + formula(sp.raw) + '</span></span>';
|
||||||
|
}).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 += '<tr class="' + (eq ? 'eq' : 'ne') + '"><td>' + e + '</td><td>' + l + '</td><td>' + r + '</td>' +
|
||||||
|
'<td>' + (eq ? '✓' : '≠') + '</td></tr>';
|
||||||
|
});
|
||||||
|
out.className = 'ceqb-out ' + (ok ? 'ok' : 'bad');
|
||||||
|
out.innerHTML = (ok ? '<div class="ceqb-msg">Уравнение сбалансировано.</div>'
|
||||||
|
: '<div class="ceqb-msg">Не сходится — выровняйте выделенные элементы.</div>') +
|
||||||
|
'<table class="ceqb-tab"><thead><tr><th>Элемент</th><th>Слева</th><th>Справа</th><th></th></tr></thead><tbody>' +
|
||||||
|
rows + '</tbody></table>';
|
||||||
|
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) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
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 += '<circle cx="' + cx + '" cy="' + cy + '" r="' + (1.6 + (i % 2)) + '" fill="rgba(255,255,255,.75)"><animate attributeName="cy" from="78" to="20" dur="' + (1.4 + i * .2) + 's" repeatCount="indefinite"/></circle>';
|
||||||
|
}
|
||||||
|
var precSvg = prec ? '<path d="M20 78 q12 7 24 0 l-2 6 q-10 5 -20 0 z" fill="' + prec + '"/>' : '';
|
||||||
|
return '<svg class="tt-svg" viewBox="0 0 64 110" width="56" height="96">'
|
||||||
|
+ '<defs><clipPath id="ttclip"><path d="M20 14 v60 a12 12 0 0 0 24 0 v-60"/></clipPath></defs>'
|
||||||
|
+ '<rect x="20" y="38" width="24" height="46" fill="' + liq + '" clip-path="url(#ttclip)" opacity=".85"/>'
|
||||||
|
+ precSvg
|
||||||
|
+ '<g clip-path="url(#ttclip)">' + bubbles + '</g>'
|
||||||
|
+ '<path d="M20 12 v62 a12 12 0 0 0 24 0 v-62" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>'
|
||||||
|
+ '<line x1="17" y1="12" x2="47" y2="12" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>'
|
||||||
|
+ (opts.label ? '<text x="32" y="104" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">' + escapeHtml(opts.label) + '</text>' : '')
|
||||||
|
+ '</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
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 =
|
||||||
|
'<div class="ind-row"><label>Индикатор</label><select class="ind-sel">' +
|
||||||
|
inds.map(function (n) { return '<option value="' + n + '"' + (n === opts.indicator ? ' selected' : '') + '>' + n + '</option>'; }).join('') +
|
||||||
|
'</select><label>pH</label><input type="range" class="ind-ph" min="0" max="14" step="0.5" value="' + (opts.ph != null ? opts.ph : 7) + '"><span class="ind-phv bd"></span></div>' +
|
||||||
|
'<div class="ind-strip"></div><div class="ind-label"></div>';
|
||||||
|
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 = 'Среда: <b>' + (v < 7 ? 'кислая' : v > 7 ? 'щелочная' : 'нейтральная') + '</b> · ' + 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 =
|
||||||
|
'<div class="cls-pool dnd-pool">' + items.map(function (it) {
|
||||||
|
return '<button class="dnd-chip cls-chip" data-id="' + it.id + '">' + it.label + '</button>';
|
||||||
|
}).join('') + '</div>' +
|
||||||
|
'<div class="dnd-zones">' + buckets.map(function (b) {
|
||||||
|
return '<div class="drop-box cls-zone" data-cat="' + b.cat + '"><h5>' + b.label + '</h5><div class="cls-items"></div></div>';
|
||||||
|
}).join('') + '</div>' +
|
||||||
|
'<div class="ceqb-actions" style="margin-top:10px"><button class="ceqb-btn primary cls-check">Проверить</button><button class="ceqb-btn cls-reset">Сброс</button></div>' +
|
||||||
|
'<div class="out cls-out" style="display:none"></div>';
|
||||||
|
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 = '<tr><th>ион</th>' + SOL_CATIONS.map(function (c) { return '<th data-cat="' + c + '">' + CAT_HTML[c] + '</th>'; }).join('') + '</tr>';
|
||||||
|
var rows = SOL_ANIONS.map(function (an) {
|
||||||
|
return '<tr><th data-an="' + an + '">' + AN_HTML[an] + '</th>' + SOL_CATIONS.map(function (c) {
|
||||||
|
var v = SOL[an][c]; var cls = v === 'P' ? 'sP' : v === 'M' ? 'sM' : v === 'H' ? 'sH' : 'sX';
|
||||||
|
return '<td class="' + cls + '" data-an="' + an + '" data-cat="' + c + '">' + SOL_LABEL[v][0] + '</td>';
|
||||||
|
}).join('') + '</tr>';
|
||||||
|
}).join('');
|
||||||
|
host.innerHTML = '<div class="sol-wrap"><table class="sol-tab"><thead>' + th + '</thead><tbody>' + rows + '</tbody></table></div>'
|
||||||
|
+ '<div class="out sol-out">Кликни по катиону и аниону — узнаешь растворимость соли/основания.</div>';
|
||||||
|
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] + ' → <b>' + SOL_LABEL[v][1] + '</b>' +
|
||||||
|
(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 = '<div class="act-row">' + ACT.map(function (m) {
|
||||||
|
return '<button class="act-cell' + (m === 'H' ? ' act-h' : '') + '" data-m="' + m + '">' + (m === 'H' ? '(H₂)' : m) + '</button>';
|
||||||
|
}).join('') + '</div><div class="act-axis"><span>← восстановит. свойства растут</span><span>активность падает →</span></div>'
|
||||||
|
+ '<div class="out act-out">Кликни по металлу — узнаешь его активность и реакцию с кислотами.</div>';
|
||||||
|
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 = '<b>' + m + '</b>: ' + withAcid + '. Вытесняет из растворов солей все металлы, стоящие <b>правее</b> (подсвечены).';
|
||||||
|
if (global.window && global.window.chem8RenderMath) try { global.window.chem8RenderMath(out); } catch (e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { el: host };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
miniPeriodic(mount, opts) — интерактивная периодическая система.
|
||||||
|
opts.highlight: 'metals'|'nonmetals'|'metalloids'|'alkali'|'alkaline'|
|
||||||
|
'halogens'|'noble'|{group:N}|{period:N}. opts.onClick(sym, info).
|
||||||
|
Стандартная раскладка 18×7; f-блок свёрнут в плейсхолдеры La и Ac.
|
||||||
|
────────────────────────────────────────────────────────────────────────── */
|
||||||
|
// [sym, group, period, Z]
|
||||||
|
var PT = [
|
||||||
|
['H',1,1,1],['He',18,1,2],
|
||||||
|
['Li',1,2,3],['Be',2,2,4],['B',13,2,5],['C',14,2,6],['N',15,2,7],['O',16,2,8],['F',17,2,9],['Ne',18,2,10],
|
||||||
|
['Na',1,3,11],['Mg',2,3,12],['Al',13,3,13],['Si',14,3,14],['P',15,3,15],['S',16,3,16],['Cl',17,3,17],['Ar',18,3,18],
|
||||||
|
['K',1,4,19],['Ca',2,4,20],['Sc',3,4,21],['Ti',4,4,22],['V',5,4,23],['Cr',6,4,24],['Mn',7,4,25],['Fe',8,4,26],['Co',9,4,27],['Ni',10,4,28],['Cu',11,4,29],['Zn',12,4,30],['Ga',13,4,31],['Ge',14,4,32],['As',15,4,33],['Se',16,4,34],['Br',17,4,35],['Kr',18,4,36],
|
||||||
|
['Rb',1,5,37],['Sr',2,5,38],['Y',3,5,39],['Zr',4,5,40],['Nb',5,5,41],['Mo',6,5,42],['Tc',7,5,43],['Ru',8,5,44],['Rh',9,5,45],['Pd',10,5,46],['Ag',11,5,47],['Cd',12,5,48],['In',13,5,49],['Sn',14,5,50],['Sb',15,5,51],['Te',16,5,52],['I',17,5,53],['Xe',18,5,54],
|
||||||
|
['Cs',1,6,55],['Ba',2,6,56],['La',3,6,57],['Hf',4,6,72],['Ta',5,6,73],['W',6,6,74],['Re',7,6,75],['Os',8,6,76],['Ir',9,6,77],['Pt',10,6,78],['Au',11,6,79],['Hg',12,6,80],['Tl',13,6,81],['Pb',14,6,82],['Bi',15,6,83],['Po',16,6,84],['At',17,6,85],['Rn',18,6,86],
|
||||||
|
['Cs',1,6,55]
|
||||||
|
];
|
||||||
|
// период 7 (главная часть)
|
||||||
|
var PT7 = [['Fr',1,7,87],['Ra',2,7,88],['Ac',3,7,89],['Rf',4,7,104],['Db',5,7,105],['Sg',6,7,106],['Bh',7,7,107],['Hs',8,7,108],['Mt',9,7,109],['Ds',10,7,110],['Rg',11,7,111],['Cn',12,7,112],['Nh',13,7,113],['Fl',14,7,114],['Mc',15,7,115],['Lv',16,7,116],['Ts',17,7,117],['Og',18,7,118]];
|
||||||
|
var PT_NAMES = { H:'Водород', He:'Гелий', Li:'Литий', Be:'Бериллий', B:'Бор', C:'Углерод', N:'Азот', O:'Кислород', F:'Фтор', Ne:'Неон', Na:'Натрий', Mg:'Магний', Al:'Алюминий', Si:'Кремний', P:'Фосфор', S:'Сера', Cl:'Хлор', Ar:'Аргон', K:'Калий', Ca:'Кальций', Fe:'Железо', Cu:'Медь', Zn:'Цинк', Br:'Бром', Ag:'Серебро', I:'Йод', Ba:'Барий', Au:'Золото', Hg:'Ртуть', Pb:'Свинец' };
|
||||||
|
var NONMETALS = { H:1, He:1, C:1, N:1, O:1, F:1, Ne:1, P:1, S:1, Cl:1, Ar:1, Se:1, Br:1, Kr:1, I:1, Xe:1, At:1, Rn:1, Ts:1, Og:1 };
|
||||||
|
var METALLOIDS = { B:1, Si:1, Ge:1, As:1, Sb:1, Te:1, Po:1 };
|
||||||
|
function ptCategory(sym, g) {
|
||||||
|
if (g === 18) return 'noble';
|
||||||
|
if (METALLOIDS[sym]) return 'metalloid';
|
||||||
|
if (NONMETALS[sym]) return 'nonmetal';
|
||||||
|
return 'metal';
|
||||||
|
}
|
||||||
|
function ptMatch(hl, sym, g, p) {
|
||||||
|
if (!hl) return false;
|
||||||
|
if (typeof hl === 'object') { if (hl.group) return g === hl.group; if (hl.period) return p === hl.period; return false; }
|
||||||
|
var cat = ptCategory(sym, g);
|
||||||
|
if (hl === 'metals') return cat === 'metal';
|
||||||
|
if (hl === 'nonmetals') return cat === 'nonmetal';
|
||||||
|
if (hl === 'metalloids') return cat === 'metalloid';
|
||||||
|
if (hl === 'noble') return g === 18;
|
||||||
|
if (hl === 'halogens') return g === 17;
|
||||||
|
if (hl === 'alkali') return g === 1 && sym !== 'H';
|
||||||
|
if (hl === 'alkaline') return g === 2;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function miniPeriodic(mount, opts) {
|
||||||
|
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||||||
|
if (!host) return null;
|
||||||
|
opts = opts || {};
|
||||||
|
var all = PT.slice(0, PT.length - 1).concat(PT7); // убрать дубль Cs-стоппер
|
||||||
|
// фильтр дубликата Cs (вставлен как маркер конца) — оставляем уникальные по Z
|
||||||
|
var seen = {}, els = [];
|
||||||
|
all.forEach(function (e) { if (!seen[e[3]]) { seen[e[3]] = 1; els.push(e); } });
|
||||||
|
function cell(e) {
|
||||||
|
var sym = e[0], g = e[1], p = e[2], z = e[3], cat = ptCategory(sym, g);
|
||||||
|
var hot = ptMatch(opts.highlight, sym, g, p);
|
||||||
|
return '<button class="pt-cell pt-' + cat + (hot ? ' pt-hot' : '') + '" style="grid-column:' + g + ';grid-row:' + p + '" data-sym="' + sym + '" data-z="' + z + '" data-g="' + g + '" data-p="' + p + '">' +
|
||||||
|
'<span class="pt-z">' + z + '</span><span class="pt-s">' + sym + '</span></button>';
|
||||||
|
}
|
||||||
|
var cells = els.map(cell).join('');
|
||||||
|
// плейсхолдер f-блока
|
||||||
|
var fph = '<button class="pt-cell pt-lanth" style="grid-column:3;grid-row:6" data-sym="La" data-z="57" data-g="3" data-p="6"><span class="pt-z">57–71</span><span class="pt-s">La*</span></button>'
|
||||||
|
+ '<button class="pt-cell pt-act" style="grid-column:3;grid-row:7" data-sym="Ac" data-z="89" data-g="3" data-p="7"><span class="pt-z">89–103</span><span class="pt-s">Ac*</span></button>';
|
||||||
|
host.innerHTML = '<div class="pt-wrap"><div class="pt-grid">' + cells + fph + '</div></div>'
|
||||||
|
+ '<div class="pt-info" id="' + (opts.infoId || 'pt-info') + '">Кликни элемент — увидишь название, $Z$ и $A_r$.</div>';
|
||||||
|
var info = host.querySelector('.pt-info');
|
||||||
|
host.querySelectorAll('.pt-cell').forEach(function (c) {
|
||||||
|
c.addEventListener('click', function () {
|
||||||
|
host.querySelectorAll('.pt-cell').forEach(function (x) { x.classList.remove('pt-sel'); });
|
||||||
|
c.classList.add('pt-sel');
|
||||||
|
var sym = c.getAttribute('data-sym'), z = c.getAttribute('data-z'), g = +c.getAttribute('data-g'), p = +c.getAttribute('data-p');
|
||||||
|
var ar = arOf(sym), cat = ptCategory(sym, g);
|
||||||
|
var catRu = cat === 'metal' ? 'металл' : cat === 'nonmetal' ? 'неметалл' : cat === 'metalloid' ? 'металлоид' : 'инертный газ';
|
||||||
|
var fam = g === 1 && sym !== 'H' ? ' · щелочной металл' : g === 2 ? ' · щёлочноземельный' : g === 17 ? ' · галоген' : g === 18 ? ' · инертный газ' : '';
|
||||||
|
info.innerHTML = '<b>' + (PT_NAMES[sym] || sym) + '</b> (' + sym + ') · Z = ' + z + (ar ? ' · A_r = ' + ar : '') + ' · группа ' + g + ', период ' + p + ' · ' + catRu + fam;
|
||||||
|
if (typeof opts.onClick === 'function') opts.onClick(sym, { z: z, g: g, p: p, ar: ar, cat: cat });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
el: host,
|
||||||
|
highlight: function (hl) {
|
||||||
|
host.querySelectorAll('.pt-cell').forEach(function (c) {
|
||||||
|
c.classList.toggle('pt-hot', ptMatch(hl, c.getAttribute('data-sym'), +c.getAttribute('data-g'), +c.getAttribute('data-p')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
Строение атома (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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
Химическая связь (Phase 5).
|
||||||
|
EN — электроотрицательность (Полинг, школьные значения). bondClass(da,db)
|
||||||
|
по разнице ЭО → тип связи. bondType(mount) — интерактивный виджет.
|
||||||
|
────────────────────────────────────────────────────────────────────────── */
|
||||||
|
var EN = {
|
||||||
|
H:2.1, Li:1.0, Be:1.5, B:2.0, C:2.5, N:3.0, O:3.5, F:4.0,
|
||||||
|
Na:0.9, Mg:1.2, Al:1.5, Si:1.8, P:2.1, S:2.5, Cl:3.0,
|
||||||
|
K:0.8, Ca:1.0, Br:2.8, I:2.5, Zn:1.6, Fe:1.8, Cu:1.9, Ag:1.9
|
||||||
|
};
|
||||||
|
function enOf(sym) { return EN[sym] != null ? EN[sym] : 2.0; }
|
||||||
|
function bondClass(a, b) {
|
||||||
|
var d = Math.abs(enOf(a) - enOf(b));
|
||||||
|
if (a !== b && (a in EN) && (b in EN) && enOf(a) <= 1.6 && enOf(b) <= 1.6) {
|
||||||
|
// два металла → металлическая
|
||||||
|
if (METALS_EN[a] && METALS_EN[b]) return { type: 'металлическая', cls: 'warn', d: d };
|
||||||
|
}
|
||||||
|
if (d >= 1.7) return { type: 'ионная', cls: 'bad', d: d };
|
||||||
|
if (d < 0.4) return { type: 'ковалентная неполярная', cls: 'good', d: d };
|
||||||
|
return { type: 'ковалентная полярная', cls: 'mid', d: d };
|
||||||
|
}
|
||||||
|
var METALS_EN = { Li:1, Be:1, Na:1, Mg:1, Al:1, K:1, Ca:1, Zn:1, Fe:1, Cu:1, Ag:1 };
|
||||||
|
|
||||||
|
function bondType(mount, opts) {
|
||||||
|
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||||||
|
if (!host) return null;
|
||||||
|
opts = opts || {};
|
||||||
|
var syms = Object.keys(EN);
|
||||||
|
function optList(sel) { return syms.map(function (s) { return '<option value="' + s + '"' + (s === sel ? ' selected' : '') + '>' + s + ' (ЭО ' + enOf(s) + ')</option>'; }).join(''); }
|
||||||
|
host.innerHTML = '<div class="fld"><label>Атом A</label><select class="bt-a">' + optList(opts.a || 'H') + '</select>'
|
||||||
|
+ '<label>Атом B</label><select class="bt-b">' + optList(opts.b || 'Cl') + '</select></div>'
|
||||||
|
+ '<div class="bt-stage"></div><div class="out bt-out"></div>';
|
||||||
|
var sa = host.querySelector('.bt-a'), sb = host.querySelector('.bt-b'), stage = host.querySelector('.bt-stage'), out = host.querySelector('.bt-out');
|
||||||
|
function upd() {
|
||||||
|
var a = sa.value, b = sb.value, r = bondClass(a, b), d = Math.round(r.d * 10) / 10;
|
||||||
|
// δ-заряды: более ЭО атом — δ−
|
||||||
|
var aMore = enOf(a) > enOf(b), polar = r.type.indexOf('полярная') >= 0;
|
||||||
|
var da = (r.type === 'ионная') ? (aMore ? '−' : '+') : (polar ? (aMore ? 'δ−' : 'δ+') : '');
|
||||||
|
var db = (r.type === 'ионная') ? (aMore ? '+' : '−') : (polar ? (aMore ? 'δ+' : 'δ−') : '');
|
||||||
|
var color = r.cls === 'good' ? 'var(--ok)' : r.cls === 'bad' ? 'var(--fail)' : 'var(--pri)';
|
||||||
|
stage.innerHTML = '<svg viewBox="0 0 240 70" class="bt-svg">'
|
||||||
|
+ '<line x1="95" y1="35" x2="145" y2="35" stroke="' + color + '" stroke-width="3"/>'
|
||||||
|
+ '<circle cx="80" cy="35" r="26" fill="' + color + '" opacity=".15" stroke="' + color + '" stroke-width="2"/>'
|
||||||
|
+ '<circle cx="160" cy="35" r="26" fill="' + color + '" opacity=".15" stroke="' + color + '" stroke-width="2"/>'
|
||||||
|
+ '<text x="80" y="40" text-anchor="middle" font-size="16" font-weight="800" fill="currentColor">' + a + '</text>'
|
||||||
|
+ '<text x="160" y="40" text-anchor="middle" font-size="16" font-weight="800" fill="currentColor">' + b + '</text>'
|
||||||
|
+ (da ? '<text x="80" y="12" text-anchor="middle" font-size="12" font-weight="800" fill="' + color + '">' + da + '</text>' : '')
|
||||||
|
+ (db ? '<text x="160" y="12" text-anchor="middle" font-size="12" font-weight="800" fill="' + color + '">' + db + '</text>' : '')
|
||||||
|
+ '</svg>';
|
||||||
|
out.className = 'out bt-out ' + (r.cls === 'good' ? 'ok' : r.cls === 'bad' ? 'bad' : '');
|
||||||
|
out.innerHTML = '<span class="bd">ΔЭО = |' + enOf(a) + ' − ' + enOf(b) + '| = <b>' + d + '</b> → связь <b>' + r.type + '</b>'
|
||||||
|
+ (r.type === 'ионная' ? '<br>Электрон полностью переходит к более электроотрицательному атому.' : polar ? '<br>Общая пара смещена к более электроотрицательному атому (' + (aMore ? a : b) + ').' : r.type.indexOf('металл') >= 0 ? '<br>Общие электроны принадлежат всем атомам («электронный газ»).' : '<br>Общая пара поделена поровну.') + '</span>';
|
||||||
|
}
|
||||||
|
sa.addEventListener('change', upd); sb.addEventListener('change', upd); upd();
|
||||||
|
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 = '<div class="fld"><label>Формула</label><input type="text" class="ox-in" value="' + (opts.formula || 'H2SO4') + '" style="width:150px;font-family:var(--mono)"><button class="btn primary ox-go">Определить</button></div>'
|
||||||
|
+ '<div class="fld" style="gap:6px"><button class="btn ox-ex" data-f="H2O">H₂O</button><button class="btn ox-ex" data-f="CO2">CO₂</button><button class="btn ox-ex" data-f="Fe2O3">Fe₂O₃</button><button class="btn ox-ex" data-f="KMnO4">KMnO₄</button><button class="btn ox-ex" data-f="HNO3">HNO₃</button></div>'
|
||||||
|
+ '<div class="out ox-out"></div>';
|
||||||
|
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 = '<span class="bd">' + Object.keys(r).map(function (el) { return el + ': <b>' + oxSign(r[el]) + '</b>'; }).join(' ') + '</span>';
|
||||||
|
}
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
geneticMap(mount) — интерактивный граф генетической связи классов веществ.
|
||||||
|
Клик по переходу (ребру) → реакция-пример. §22.
|
||||||
|
────────────────────────────────────────────────────────────────────────── */
|
||||||
|
var GM_NODES = [
|
||||||
|
{ id: 'me', t: 'Металл', x: 20, y: 22, c: '#0d9488' },
|
||||||
|
{ id: 'mox', t: 'Осн. оксид', x: 120, y: 22, c: '#0d9488' },
|
||||||
|
{ id: 'base', t: 'Основание', x: 228, y: 22, c: '#0d9488' },
|
||||||
|
{ id: 'salt', t: 'Соль', x: 336, y: 55, c: '#d97706' },
|
||||||
|
{ id: 'nm', t: 'Неметалл', x: 20, y: 90, c: '#2563eb' },
|
||||||
|
{ id: 'nox', t: 'Кисл. оксид', x: 120, y: 90, c: '#2563eb' },
|
||||||
|
{ id: 'acid', t: 'Кислота', x: 228, y: 90, c: '#2563eb' }
|
||||||
|
];
|
||||||
|
var GM_EDGES = [
|
||||||
|
{ f: 'me', t: 'mox', r: '2Mg + O2 -> 2MgO', d: 'Металл + кислород → основный оксид' },
|
||||||
|
{ f: 'mox', t: 'base', r: 'CaO + H2O -> Ca(OH)2', d: 'Основный оксид + вода → основание (щёлочь)' },
|
||||||
|
{ f: 'base', t: 'salt', r: '2NaOH + H2SO4 -> Na2SO4 + 2H2O', d: 'Основание + кислота → соль + вода (нейтрализация)' },
|
||||||
|
{ f: 'nm', t: 'nox', r: 'S + O2 -> SO2', d: 'Неметалл + кислород → кислотный оксид' },
|
||||||
|
{ f: 'nox', t: 'acid', r: 'SO3 + H2O -> H2SO4', d: 'Кислотный оксид + вода → кислота' },
|
||||||
|
{ f: 'acid', t: 'salt', r: '2HCl + Ca(OH)2 -> CaCl2 + 2H2O', d: 'Кислота + основание → соль + вода' }
|
||||||
|
];
|
||||||
|
function geneticMap(mount, opts) {
|
||||||
|
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||||||
|
if (!host) return null;
|
||||||
|
var byId = {}; GM_NODES.forEach(function (n) { byId[n.id] = n; });
|
||||||
|
function cx(n) { return n.x + 44; } function cy(n) { return n.y + 16; }
|
||||||
|
var edgesSvg = GM_EDGES.map(function (e, i) {
|
||||||
|
var a = byId[e.f], b = byId[e.t];
|
||||||
|
return '<line class="gm-edge" data-i="' + i + '" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '" stroke="var(--muted,#888)" stroke-width="2.5"/>';
|
||||||
|
}).join('');
|
||||||
|
var nodesSvg = GM_NODES.map(function (n) {
|
||||||
|
return '<g><rect x="' + n.x + '" y="' + n.y + '" width="88" height="32" rx="8" fill="' + n.c + '" opacity=".16" stroke="' + n.c + '" stroke-width="1.5"/>'
|
||||||
|
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 4) + '" text-anchor="middle" font-size="11" font-weight="800" fill="currentColor">' + n.t + '</text></g>';
|
||||||
|
}).join('');
|
||||||
|
host.innerHTML = '<div class="gm-wrap"><svg viewBox="0 0 430 130" class="gm-svg">' + edgesSvg + nodesSvg + '</svg></div>'
|
||||||
|
+ '<div class="out gm-out">Кликни по стрелке-переходу — увидишь реакцию-пример.</div>';
|
||||||
|
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');
|
||||||
|
var e = GM_EDGES[+ln.getAttribute('data-i')];
|
||||||
|
out.className = 'out gm-out ok';
|
||||||
|
out.innerHTML = '<b>' + e.d + '</b><br><span class="bd">' + chemEq(e.r) + '</span>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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 '<line class="gm-edge" data-i="' + i + '" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '" stroke="var(--muted,#888)" stroke-width="2.5"/>';
|
||||||
|
}).join('');
|
||||||
|
var nodesSvg = nodes.map(function (n) {
|
||||||
|
var c = n.c || 'var(--pri,#d97706)';
|
||||||
|
return '<g><rect x="' + n.x + '" y="' + n.y + '" width="88" height="32" rx="8" fill="' + c + '" opacity=".16" stroke="' + c + '" stroke-width="1.5"/>'
|
||||||
|
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 4) + '" text-anchor="middle" font-size="10.5" font-weight="800" fill="currentColor">' + n.t + '</text></g>';
|
||||||
|
}).join('');
|
||||||
|
host.innerHTML = '<div class="gm-wrap"><svg viewBox="0 0 ' + W0 + ' ' + H0 + '" class="gm-svg">' + edgesSvg + nodesSvg + '</svg></div>'
|
||||||
|
+ '<div class="out gm-out">Кликни по связи — увидишь, как понятия главы связаны.</div>';
|
||||||
|
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 = '<b>' + edges[+ln.getAttribute('data-i')].label + '</b>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { el: host };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────────────
|
||||||
|
dissociationAnim(mount, {substance}) — анимация растворения/диссоциации:
|
||||||
|
вещество распадается на ионы, окружённые молекулами воды. §47.
|
||||||
|
────────────────────────────────────────────────────────────────────────── */
|
||||||
|
var DISS = {
|
||||||
|
NaCl: { cat: 'Na⁺', an: 'Cl⁻', cc: '#d97706', ac: '#0891b2' },
|
||||||
|
KCl: { cat: 'K⁺', an: 'Cl⁻', cc: '#7c3aed', ac: '#0891b2' },
|
||||||
|
CuSO4: { cat: 'Cu²⁺', an: 'SO₄²⁻', cc: '#0891b2', ac: '#059669' },
|
||||||
|
HCl: { cat: 'H⁺', an: 'Cl⁻', cc: '#dc2626', ac: '#0891b2' }
|
||||||
|
};
|
||||||
|
function dissociationAnim(mount, opts) {
|
||||||
|
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||||||
|
if (!host) return null;
|
||||||
|
opts = opts || {};
|
||||||
|
var subs = Object.keys(DISS);
|
||||||
|
host.innerHTML = '<div class="fld"><label>Вещество</label><select class="ds-sel">' +
|
||||||
|
subs.map(function (s) { return '<option value="' + s + '"' + (s === opts.substance ? ' selected' : '') + '>' + formula(s) + '</option>'; }).join('') + '</select></div>'
|
||||||
|
+ '<div class="ds-stage"></div><div class="out ds-out"></div>';
|
||||||
|
var sel = host.querySelector('.ds-sel'), stage = host.querySelector('.ds-stage'), out = host.querySelector('.ds-out');
|
||||||
|
function draw() {
|
||||||
|
var s = sel.value, d = DISS[s];
|
||||||
|
// молекулы воды (фон) + катион + анион, разлетающиеся
|
||||||
|
var water = '';
|
||||||
|
for (var i = 0; i < 7; i++) { var wx = 30 + i * 35, wy = 25 + (i % 3) * 30; water += '<circle cx="' + wx + '" cy="' + wy + '" r="3" fill="#60a5fa" opacity=".5"/>'; }
|
||||||
|
stage.innerHTML = '<svg viewBox="0 0 270 100" class="ds-svg">'
|
||||||
|
+ '<rect x="6" y="6" width="258" height="88" rx="12" fill="#0891b2" opacity=".07" stroke="#0891b2" stroke-width="1.5"/>' + water
|
||||||
|
+ '<circle cx="135" cy="50" r="17" fill="' + d.cc + '" opacity=".85"><animate attributeName="cx" values="135;70;135" dur="3s" repeatCount="indefinite"/></circle>'
|
||||||
|
+ '<text x="135" y="55" text-anchor="middle" font-size="11" font-weight="800" fill="#fff"><animate attributeName="x" values="135;70;135" dur="3s" repeatCount="indefinite"/>' + d.cat + '</text>'
|
||||||
|
+ '<circle cx="135" cy="50" r="17" fill="' + d.ac + '" opacity=".85"><animate attributeName="cx" values="135;200;135" dur="3s" repeatCount="indefinite"/></circle>'
|
||||||
|
+ '<text x="135" y="55" text-anchor="middle" font-size="10" font-weight="800" fill="#fff"><animate attributeName="x" values="135;200;135" dur="3s" repeatCount="indefinite"/>' + d.an + '</text>'
|
||||||
|
+ '</svg>';
|
||||||
|
out.className = 'out ds-out ok';
|
||||||
|
out.innerHTML = '<span class="bd">' + formula(s) + ' → ' + d.cat + ' + ' + d.an + '<br>Молекулы воды окружают ионы и «растаскивают» их (гидратация).</span>';
|
||||||
|
}
|
||||||
|
sel.addEventListener('change', draw); draw();
|
||||||
|
return { el: host };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */
|
||||||
|
function notImplemented(name) {
|
||||||
|
return function () {
|
||||||
|
if (global.console && console.warn) {
|
||||||
|
console.warn('[Chem8] ' + name + ' ещё не реализован (Phase 0 заглушка)');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var Chem8 = {
|
||||||
|
// готово (Phase 0)
|
||||||
|
formula: formula,
|
||||||
|
ionLabel: ionLabel,
|
||||||
|
chemEq: chemEq,
|
||||||
|
toSub: toSub,
|
||||||
|
toSup: toSup,
|
||||||
|
// готово (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 — балансировщик уравнений
|
||||||
|
// готово (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 — ряд активности металлов
|
||||||
|
// готово (Phase 3 — периодический закон)
|
||||||
|
miniPeriodic: miniPeriodic, // §26,28,34 — интерактивная ПСХЭ с подсветкой
|
||||||
|
// готово (Phase 4 — строение атома)
|
||||||
|
atomShell: atomShell, // §29,33 — модель атома (слои электронов)
|
||||||
|
shellConfig: shellConfig, // распределение электронов по слоям
|
||||||
|
nuclide: nuclide, // §30 — A=Z+N, нуклид
|
||||||
|
zSym: zSym, // Z → символ элемента
|
||||||
|
// готово (Phase 5 — химическая связь)
|
||||||
|
bondType: bondType, // §37,38 — ЭО → тип связи
|
||||||
|
bondClass: bondClass, // классификация связи по ΔЭО
|
||||||
|
enOf: enOf, // электроотрицательность
|
||||||
|
// готово (Phase 6 — ОВР)
|
||||||
|
oxStateCalc: oxStateCalc, // §42 — калькулятор степени окисления
|
||||||
|
oxStates: oxStates, // степени окисления (чистая функция)
|
||||||
|
// готово (Phase 8/U3,U6 — апгрейд)
|
||||||
|
geneticMap: geneticMap, // §22 — генетическая карта-граф классов
|
||||||
|
conceptMap: conceptMap, // финалы глав — карта связей понятий (U6)
|
||||||
|
dissociationAnim: dissociationAnim, // §47 — анимация растворения/диссоциации
|
||||||
|
// редокс-баланс §44 реализован пошагово в chem8_ch5_widgets (преднабор)
|
||||||
|
redoxBalancer: notImplemented('redoxBalancer'),
|
||||||
|
orbitalDiagram: notImplemented('orbitalDiagram') // §33 — покрыто atomShell
|
||||||
|
};
|
||||||
|
|
||||||
|
global.Chem8 = Chem8;
|
||||||
|
})(typeof window !== 'undefined' ? window : this);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* LabLoader — ленивый загрузчик кода симуляций (контент-движок, Фаза 3).
|
||||||
|
*
|
||||||
|
* Тяжёлый код симуляций (~2.5 МБ) и three.js (~600 КБ) больше НЕ грузятся на старте.
|
||||||
|
* При открытии симуляции LabLoader.ensure(id) подгружает её файлы (по манифесту
|
||||||
|
* window.SIM_DEPS из _sim_deps.js) и, при необходимости, three.js — затем резолвит.
|
||||||
|
*
|
||||||
|
* Гарантия корректности (самовосстановление): если после загрузки указанных файлов
|
||||||
|
* глобальная open-функция (SIM_DEPS[id].open, напр. "_openPendulum") всё ещё не
|
||||||
|
* определена, грузятся ВСЕ ленивые файлы (window.LAB_LAZY_FILES). Поэтому ошибка в
|
||||||
|
* манифесте не может «сломать» симуляцию — в худшем случае грузится больше файлов
|
||||||
|
* (поведение как до Фазы 3). Манифест лишь оптимизирует объём загрузки.
|
||||||
|
*
|
||||||
|
* Все загрузки кешируются (по URL) и дедуплицируются.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var BASE = '/js/labs/';
|
||||||
|
var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js';
|
||||||
|
var _cache = {}; // url -> Promise
|
||||||
|
var _allLoaded = false;
|
||||||
|
|
||||||
|
function loadScript(url) {
|
||||||
|
if (_cache[url]) return _cache[url];
|
||||||
|
_cache[url] = new Promise(function (resolve, reject) {
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = url;
|
||||||
|
s.async = false; // сохранить порядок при добавлении нескольких сразу
|
||||||
|
s.onload = function () { resolve(url); };
|
||||||
|
s.onerror = function () { delete _cache[url]; reject(new Error('LabLoader: не удалось загрузить ' + url)); };
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
return _cache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureThree() {
|
||||||
|
if (typeof window.THREE !== 'undefined') return Promise.resolve();
|
||||||
|
return loadScript(THREE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFiles(files) {
|
||||||
|
return Promise.all((files || []).map(function (f) { return loadScript(BASE + f); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAllLazy() {
|
||||||
|
if (_allLoaded) return Promise.resolve();
|
||||||
|
var list = window.LAB_LAZY_FILES || [];
|
||||||
|
return loadFiles(list).then(function () { _allLoaded = true; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure(id): загрузить всё необходимое для симуляции id, вернуть Promise.
|
||||||
|
function ensure(id) {
|
||||||
|
var dep = (window.SIM_DEPS && window.SIM_DEPS[id]) || null;
|
||||||
|
if (!dep) {
|
||||||
|
// нет манифеста для id — безопасно грузим всё
|
||||||
|
return loadAllLazy();
|
||||||
|
}
|
||||||
|
var p = dep.three ? ensureThree() : Promise.resolve();
|
||||||
|
return p
|
||||||
|
.then(function () { return loadFiles(dep.files); })
|
||||||
|
.then(function () {
|
||||||
|
var openName = dep.open;
|
||||||
|
if (openName && typeof window[openName] !== 'function') {
|
||||||
|
if (window.console) console.warn('[LabLoader] самовосстановление для "' + id + '": ' + openName + ' не найдена после загрузки ' + JSON.stringify(dep.files) + ' — гружу все ленивые файлы');
|
||||||
|
return loadAllLazy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.LabLoader = {
|
||||||
|
ensure: ensure,
|
||||||
|
ensureThree: ensureThree,
|
||||||
|
loadScript: loadScript,
|
||||||
|
loadAllLazy: loadAllLazy
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* Контент-движок, Фаза 1 — data-driven регистрация ВСЕХ симуляций в LabRegistry.
|
||||||
|
*
|
||||||
|
* Вместо ручного переписывания 40 манифестов модуль строит их из единых источников:
|
||||||
|
* - метаданные (id/cat/title/desc) и preview — из массива SIMS (lab-glue.js);
|
||||||
|
* - теория — из объекта THEORY (lab-init.js);
|
||||||
|
* - поведение open(ctx) — из карты OPEN ниже (обёртки над глобальными _openXxx).
|
||||||
|
* Это структурно гарантирует паритет с прежним каталогом и диспетчеризацией.
|
||||||
|
*
|
||||||
|
* Подключается ПОСЛЕДНИМ среди labs-скриптов (defer), поэтому SIMS, THEORY и все
|
||||||
|
* _openXxx уже определены. Останов/закрытие симуляций по-прежнему выполняет
|
||||||
|
* «дробовик» _pauseAllSims()/closeSim() (точный паритет) — поэтому stop/destroy
|
||||||
|
* в манифестах не задаются на этом этапе.
|
||||||
|
*
|
||||||
|
* После регистрации if-цепочка в openSim() становится мёртвой и удалена.
|
||||||
|
*
|
||||||
|
* В Фазе 1 заменил пилотный _pilots.js. SIMS/THEORY остаются источниками данных
|
||||||
|
* (SIMS → БД в Фазе 4, THEORY сворачивается в манифесты позже).
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
if (!window.LabRegistry) return;
|
||||||
|
if (typeof SIMS === 'undefined') return;
|
||||||
|
var R = window.LabRegistry;
|
||||||
|
var T = (typeof THEORY !== 'undefined') ? THEORY : {};
|
||||||
|
|
||||||
|
// id -> open(ctx). ctx.arg — параметр deep-link (после двоеточия): stereo:cube и т.п.
|
||||||
|
var OPEN = {
|
||||||
|
graph: function (c) { _openGraph(); },
|
||||||
|
projectile: function (c) { _openProjectile(); },
|
||||||
|
collision: function (c) { _openCollision(); },
|
||||||
|
triangle: function (c) { _openTriangle(); },
|
||||||
|
trigcircle: function (c) { _openTrigCircle(); },
|
||||||
|
emfield: function (c) { _openEMField(c.arg || 'E'); },
|
||||||
|
molphys: function (c) { _openMolPhys(c.arg); },
|
||||||
|
circuit: function (c) { _openCircuit(); },
|
||||||
|
chemistry: function (c) { _openChemistry(c.arg); },
|
||||||
|
dynamics: function (c) { _openDynamics(c.arg); },
|
||||||
|
crystal: function (c) { _openCrystal(); },
|
||||||
|
orbitals: function (c) { _openOrbitals(); },
|
||||||
|
stereo: function (c) { _openStereo(c.arg); },
|
||||||
|
chemsandbox: function (c) { _openChemSandbox(); },
|
||||||
|
celldivision: function (c) { _openCellDivision(); },
|
||||||
|
photosynthesis: function (c) { _openPhotosynthesis(); },
|
||||||
|
angrybirds: function (c) { _openAngryBirds(); },
|
||||||
|
quadratic: function (c) { _openQuadratic(); },
|
||||||
|
normaldist: function (c) { _openNormalDist(); },
|
||||||
|
graphtransform: function (c) { _openGraphTransform(); },
|
||||||
|
pendulum: function (c) { _openPendulum(); },
|
||||||
|
equilibrium: function (c) { _openEquilibrium(); },
|
||||||
|
opticsbench: function (c) { _openOpticsBench(c.arg || 'lens'); },
|
||||||
|
isoprocess: function (c) { _openIsoprocess(); },
|
||||||
|
titration: function (c) { _openTitration(); },
|
||||||
|
probability: function (c) { _openProbability(); },
|
||||||
|
bohratom: function (c) { _openBohrAtom(); },
|
||||||
|
electrolysis: function (c) { _openElectrolysis(); },
|
||||||
|
race: function (c) { _openRace(); },
|
||||||
|
waves: function (c) { _openWaves(); },
|
||||||
|
hydrostatics: function (c) { _openHydro(c.arg); },
|
||||||
|
radioactive: function (c) { _openRadioactive(); },
|
||||||
|
geometry: function (c) { _openGeometry(); },
|
||||||
|
logic: function (c) { _openLogic(); },
|
||||||
|
heatengine: function (c) { _openHeatEngine(); },
|
||||||
|
stoichiometry: function (c) { _openStoich(); },
|
||||||
|
qualanalysis: function (c) { _openQualAnalysis(); },
|
||||||
|
periodic: function (c) { _openPeriodic(); },
|
||||||
|
organic: function (c) { _openOrganic(); },
|
||||||
|
solutions: function (c) { _openSolutions(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
SIMS.forEach(function (s) {
|
||||||
|
if (!s.id) return; // "Скоро" — карточка без id
|
||||||
|
var open = OPEN[s.id];
|
||||||
|
if (!open) { // подстраховка: незамапленный id оставляем legacy-пути
|
||||||
|
if (window.console) console.warn('[LabRegistry] нет open() для', s.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
R.register({
|
||||||
|
id: s.id,
|
||||||
|
cat: s.cat,
|
||||||
|
title: s.title,
|
||||||
|
desc: s.desc,
|
||||||
|
preview: s.preview, // уже готовая SVG-строка (P_* вычислены в SIMS)
|
||||||
|
theory: T[s.id] || null,
|
||||||
|
// Фаза 3: ленивая загрузка кода. LabLoader.ensure(id) подгружает файлы
|
||||||
|
// симуляции (+ three.js при необходимости), затем выполняется raw-open.
|
||||||
|
// Если LabLoader недоступен — открываем синхронно как раньше (фолбэк).
|
||||||
|
open: (function (rawOpen, simId) {
|
||||||
|
return function (c) {
|
||||||
|
if (window.LabLoader && window.LabLoader.ensure) {
|
||||||
|
return window.LabLoader.ensure(simId).then(function () { rawOpen(c); });
|
||||||
|
}
|
||||||
|
rawOpen(c);
|
||||||
|
};
|
||||||
|
})(open, s.id)
|
||||||
|
// stop/destroy: глобальный «дробовик» _pauseAllSims()/closeSim() — паритет
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Алиасы deep-link → канонический id[:arg]. Диспетчер openSim() нормализует их
|
||||||
|
// перед обращением к реестру (карточек у алиасов нет — только прямые ссылки).
|
||||||
|
window.LAB_SIM_ALIASES = {
|
||||||
|
magnetic: 'emfield:B',
|
||||||
|
coulomb: 'emfield:E',
|
||||||
|
thinlens: 'opticsbench:lens',
|
||||||
|
mirrors: 'opticsbench:mirror',
|
||||||
|
refraction: 'opticsbench:refraction'
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* LabRegistry — единый реестр симуляций лаборатории (контент-движок).
|
||||||
|
*
|
||||||
|
* Цель: симуляции описываются декларативным манифестом и сами себя регистрируют,
|
||||||
|
* вместо захардкоженных массивов (SIMS), if-цепочек (openSim) и объектов (THEORY).
|
||||||
|
*
|
||||||
|
* Манифест:
|
||||||
|
* {
|
||||||
|
* id: 'pendulum', // уникальный, без ':arg'
|
||||||
|
* cat: 'phys', // math | phys | chem | bio | game
|
||||||
|
* title: 'Маятник',
|
||||||
|
* desc: 'Колебания, период…',
|
||||||
|
* preview: string | function(), // SVG-разметка карточки (функция вычисляется лениво)
|
||||||
|
* theory: { title, sections[] },// объект для панели теории (как в THEORY)
|
||||||
|
* bodyId: 'sim-pendulum', // (опц.) id тела; mount() — для ленивого создания DOM (Фаза 2)
|
||||||
|
* mount: function(host){}, // (опц.) ленивое монтирование тела
|
||||||
|
* open: function(ctx){}, // ctx = { id, arg } — открыть/инициализировать
|
||||||
|
* stop: function(){}, // (опц.) остановить анимации (не разрушая)
|
||||||
|
* destroy: function(){}, // (опц.) полностью закрыть; по умолчанию == stop
|
||||||
|
* subject, grade, topics // (опц.) курикулумные поля (Фаза 5)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Загружается ПЕРВЫМ среди labs-скриптов, чтобы window.LabRegistry существовал
|
||||||
|
* к моменту исполнения тел остальных модулей.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var _list = []; // манифесты в порядке регистрации
|
||||||
|
var _byId = {}; // id -> манифест
|
||||||
|
var _active = null; // текущая открытая симуляция
|
||||||
|
|
||||||
|
function _baseId(id) {
|
||||||
|
return id == null ? id : String(id).split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function register(m) {
|
||||||
|
if (!m || !m.id) return null;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(_byId, m.id)) {
|
||||||
|
// перерегистрация: заменить на месте, сохранив позицию
|
||||||
|
for (var i = 0; i < _list.length; i++) {
|
||||||
|
if (_list[i].id === m.id) { _list[i] = m; break; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_list.push(m);
|
||||||
|
}
|
||||||
|
_byId[m.id] = m;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(id) {
|
||||||
|
var b = _baseId(id);
|
||||||
|
return Object.prototype.hasOwnProperty.call(_byId, b) ? _byId[b] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function has(id) { return !!get(id); }
|
||||||
|
|
||||||
|
function all() { return _list.slice(); }
|
||||||
|
|
||||||
|
function setActive(m) { _active = m || null; }
|
||||||
|
|
||||||
|
function stopActive() {
|
||||||
|
if (_active && typeof _active.stop === 'function') {
|
||||||
|
try { _active.stop(); } catch (e) { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyActive() {
|
||||||
|
if (_active) {
|
||||||
|
if (typeof _active.destroy === 'function') {
|
||||||
|
try { _active.destroy(); } catch (e) { /* noop */ }
|
||||||
|
} else if (typeof _active.stop === 'function') {
|
||||||
|
try { _active.stop(); } catch (e) { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_active = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function active() { return _active; }
|
||||||
|
|
||||||
|
// Разрешить preview (строка или функция) в готовую разметку.
|
||||||
|
function resolvePreview(m) {
|
||||||
|
if (!m) return '';
|
||||||
|
var p = m.preview;
|
||||||
|
if (typeof p === 'function') {
|
||||||
|
try { return p() || ''; } catch (e) { return ''; }
|
||||||
|
}
|
||||||
|
return p || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.LabRegistry = {
|
||||||
|
register: register,
|
||||||
|
get: get,
|
||||||
|
has: has,
|
||||||
|
all: all,
|
||||||
|
setActive: setActive,
|
||||||
|
stopActive: stopActive,
|
||||||
|
destroyActive: destroyActive,
|
||||||
|
active: active,
|
||||||
|
resolvePreview: resolvePreview
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
'use strict';
|
||||||
|
/* Контент-движок, Фаза 3 — манифест зависимостей симуляций (СГЕНЕРИРОВАН).
|
||||||
|
id -> { open: имя глобальной _openX, files: [ленивые файлы], three: нужен ли three.js }.
|
||||||
|
Файлы загружаются лениво по клику (см. _loader.js). three.js — только для 3D-симуляций.
|
||||||
|
Самовосстановление в _loader: если после загрузки open-функция не определена,
|
||||||
|
грузятся ВСЕ ленивые файлы -> корректность не зависит от точности манифеста.
|
||||||
|
Регенерация: node tools/gen-sim-deps.js (см. CONTEXT). НЕ редактировать вручную. */
|
||||||
|
window.SIM_DEPS = {
|
||||||
|
"graph": {
|
||||||
|
"open": "_openGraph",
|
||||||
|
"files": [],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"projectile": {
|
||||||
|
"open": "_openProjectile",
|
||||||
|
"files": [
|
||||||
|
"projectile.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"collision": {
|
||||||
|
"open": "_openCollision",
|
||||||
|
"files": [
|
||||||
|
"collision.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"triangle": {
|
||||||
|
"open": "_openTriangle",
|
||||||
|
"files": [
|
||||||
|
"triangle.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"trigcircle": {
|
||||||
|
"open": "_openTrigCircle",
|
||||||
|
"files": [
|
||||||
|
"trigcircle.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"emfield": {
|
||||||
|
"open": "_openEMField",
|
||||||
|
"files": [
|
||||||
|
"emfield.js",
|
||||||
|
"logic.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"molphys": {
|
||||||
|
"open": "_openMolPhys",
|
||||||
|
"files": [
|
||||||
|
"brownian.js",
|
||||||
|
"diffusion.js",
|
||||||
|
"gas.js",
|
||||||
|
"states.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"circuit": {
|
||||||
|
"open": "_openCircuit",
|
||||||
|
"files": [
|
||||||
|
"circuit.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"chemistry": {
|
||||||
|
"open": "_openChemistry",
|
||||||
|
"files": [
|
||||||
|
"circuit.js",
|
||||||
|
"flask.js",
|
||||||
|
"ionexchange.js",
|
||||||
|
"reactions.js",
|
||||||
|
"redox.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"dynamics": {
|
||||||
|
"open": "_openDynamics",
|
||||||
|
"files": [
|
||||||
|
"forcesandbox.js",
|
||||||
|
"newton.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"crystal": {
|
||||||
|
"open": "_openCrystal",
|
||||||
|
"files": [
|
||||||
|
"crystal.js"
|
||||||
|
],
|
||||||
|
"three": true
|
||||||
|
},
|
||||||
|
"orbitals": {
|
||||||
|
"open": "_openOrbitals",
|
||||||
|
"files": [
|
||||||
|
"orbitals.js"
|
||||||
|
],
|
||||||
|
"three": true
|
||||||
|
},
|
||||||
|
"stereo": {
|
||||||
|
"open": "_openStereo",
|
||||||
|
"files": [
|
||||||
|
"stereo.js"
|
||||||
|
],
|
||||||
|
"three": true
|
||||||
|
},
|
||||||
|
"chemsandbox": {
|
||||||
|
"open": "_openChemSandbox",
|
||||||
|
"files": [
|
||||||
|
"chemsandbox.js",
|
||||||
|
"collision.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"celldivision": {
|
||||||
|
"open": "_openCellDivision",
|
||||||
|
"files": [
|
||||||
|
"celldivision.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"photosynthesis": {
|
||||||
|
"open": "_openPhotosynthesis",
|
||||||
|
"files": [
|
||||||
|
"photosynthesis.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"angrybirds": {
|
||||||
|
"open": "_openAngryBirds",
|
||||||
|
"files": [
|
||||||
|
"angrybirds.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"quadratic": {
|
||||||
|
"open": "_openQuadratic",
|
||||||
|
"files": [
|
||||||
|
"quadratic.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"normaldist": {
|
||||||
|
"open": "_openNormalDist",
|
||||||
|
"files": [
|
||||||
|
"normaldist.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"graphtransform": {
|
||||||
|
"open": "_openGraphTransform",
|
||||||
|
"files": [
|
||||||
|
"graphtransform.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"pendulum": {
|
||||||
|
"open": "_openPendulum",
|
||||||
|
"files": [
|
||||||
|
"pendulum.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"equilibrium": {
|
||||||
|
"open": "_openEquilibrium",
|
||||||
|
"files": [
|
||||||
|
"equilibrium.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"opticsbench": {
|
||||||
|
"open": "_openOpticsBench",
|
||||||
|
"files": [
|
||||||
|
"opticsbench.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"isoprocess": {
|
||||||
|
"open": "_openIsoprocess",
|
||||||
|
"files": [
|
||||||
|
"isoprocess.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"titration": {
|
||||||
|
"open": "_openTitration",
|
||||||
|
"files": [
|
||||||
|
"titration.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"probability": {
|
||||||
|
"open": "_openProbability",
|
||||||
|
"files": [
|
||||||
|
"probability.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"bohratom": {
|
||||||
|
"open": "_openBohrAtom",
|
||||||
|
"files": [
|
||||||
|
"bohratom.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"electrolysis": {
|
||||||
|
"open": "_openElectrolysis",
|
||||||
|
"files": [
|
||||||
|
"electrolysis.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"race": {
|
||||||
|
"open": "_openRace",
|
||||||
|
"files": [
|
||||||
|
"race.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"waves": {
|
||||||
|
"open": "_openWaves",
|
||||||
|
"files": [
|
||||||
|
"waves.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"hydrostatics": {
|
||||||
|
"open": "_openHydro",
|
||||||
|
"files": [
|
||||||
|
"hydrostatics.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"radioactive": {
|
||||||
|
"open": "_openRadioactive",
|
||||||
|
"files": [
|
||||||
|
"radioactive.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"open": "_openGeometry",
|
||||||
|
"files": [
|
||||||
|
"geometry.js",
|
||||||
|
"triangle.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"logic": {
|
||||||
|
"open": "_openLogic",
|
||||||
|
"files": [
|
||||||
|
"logic.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"heatengine": {
|
||||||
|
"open": "_openHeatEngine",
|
||||||
|
"files": [
|
||||||
|
"heatengine.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"stoichiometry": {
|
||||||
|
"open": "_openStoich",
|
||||||
|
"files": [
|
||||||
|
"stoichiometry.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"qualanalysis": {
|
||||||
|
"open": "_openQualAnalysis",
|
||||||
|
"files": [
|
||||||
|
"qualanalysis.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"periodic": {
|
||||||
|
"open": "_openPeriodic",
|
||||||
|
"files": [
|
||||||
|
"_periodic_data.js",
|
||||||
|
"periodic.js"
|
||||||
|
],
|
||||||
|
"three": true
|
||||||
|
},
|
||||||
|
"organic": {
|
||||||
|
"open": "_openOrganic",
|
||||||
|
"files": [
|
||||||
|
"organic.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
},
|
||||||
|
"solutions": {
|
||||||
|
"open": "_openSolutions",
|
||||||
|
"files": [
|
||||||
|
"solutions.js"
|
||||||
|
],
|
||||||
|
"three": false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.LAB_LAZY_FILES = ["angrybirds.js","bohratom.js","brownian.js","celldivision.js","chemsandbox.js","circuit.js","collision.js","crystal.js","diffusion.js","electrolysis.js","emfield.js","equilibrium.js","flask.js","forcesandbox.js","gas.js","geometry.js","graphtransform.js","heatengine.js","hydrostatics.js","ionexchange.js","isoprocess.js","logic.js","newton.js","normaldist.js","opticsbench.js","orbitals.js","organic.js","pendulum.js","periodic.js","photosynthesis.js","probability.js","projectile.js","quadratic.js","qualanalysis.js","race.js","radioactive.js","reactions.js","redox.js","solutions.js","states.js","stereo.js","stoichiometry.js","titration.js","triangle.js","trigcircle.js","waves.js","_periodic_data.js"];
|
||||||
@@ -20,11 +20,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSims() {
|
function renderSims() {
|
||||||
const base = _catFilter === 'all' ? SIMS : SIMS.filter(s => s.cat === _catFilter);
|
// Контент-движок: мёрж код-реестра поверх legacy SIMS.
|
||||||
|
// Порядок берём из SIMS; для мигрированных id используем манифест реестра;
|
||||||
|
// registry-only записи добавляем в конец.
|
||||||
|
const _reg = (window.LabRegistry ? window.LabRegistry.all() : []);
|
||||||
|
const _regById = {};
|
||||||
|
_reg.forEach(m => { _regById[m.id] = m; });
|
||||||
|
const _seen = {};
|
||||||
|
const _merged = [];
|
||||||
|
SIMS.forEach(s => {
|
||||||
|
_merged.push(s.id && _regById[s.id] ? _regById[s.id] : s);
|
||||||
|
if (s.id) _seen[s.id] = 1;
|
||||||
|
});
|
||||||
|
_reg.forEach(m => { if (!_seen[m.id]) _merged.push(m); });
|
||||||
|
|
||||||
|
const base = _catFilter === 'all' ? _merged : _merged.filter(s => s.cat === _catFilter);
|
||||||
const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id));
|
const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id));
|
||||||
document.getElementById('sim-grid').innerHTML = list.map(s => `
|
document.getElementById('sim-grid').innerHTML = list.map(s => `
|
||||||
<div class="sim-card ${s.id ? '' : 'soon'}" ${s.id ? `onclick="openSim('${s.id}')"` : ''}>
|
<div class="sim-card ${s.id ? '' : 'soon'}" ${s.id ? `onclick="openSim('${s.id}')"` : ''}>
|
||||||
${s.preview}
|
${window.LabRegistry ? window.LabRegistry.resolvePreview(s) : s.preview}
|
||||||
<div class="sim-body">
|
<div class="sim-body">
|
||||||
<div class="sim-cat ${s.cat}">${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Химия' : s.cat === 'bio' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg> Биология' : s.cat === 'game' ? '<svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры' : LS.icon('zap',14) + ' Физика'}</div>
|
<div class="sim-cat ${s.cat}">${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Химия' : s.cat === 'bio' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg> Биология' : s.cat === 'game' ? '<svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры' : LS.icon('zap',14) + ' Физика'}</div>
|
||||||
<div class="sim-title">${s.title}</div>
|
<div class="sim-title">${s.title}</div>
|
||||||
@@ -935,7 +949,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadTheory(simId) {
|
function loadTheory(simId) {
|
||||||
const t = THEORY[simId];
|
// Контент-движок: теория мигрированных симуляций берётся из манифеста реестра.
|
||||||
|
const _rm = window.LabRegistry ? window.LabRegistry.get(simId) : null;
|
||||||
|
const t = (_rm && _rm.theory) ? _rm.theory : THEORY[simId];
|
||||||
const el = document.getElementById('theory-content');
|
const el = document.getElementById('theory-content');
|
||||||
if (!t) { el.innerHTML = '<div class="tp-text" style="text-align:center;padding:40px 0;color:var(--text-3)">Теория для этой симуляции пока не добавлена</div>'; return; }
|
if (!t) { el.innerHTML = '<div class="tp-text" style="text-align:center;padding:40px 0;color:var(--text-3)">Теория для этой симуляции пока не добавлена</div>'; return; }
|
||||||
let html = `<div class="tp-title">${LS.icon('book-open',16)} ${t.title}</div>`;
|
let html = `<div class="tp-title">${LS.icon('book-open',16)} ${t.title}</div>`;
|
||||||
@@ -955,6 +971,58 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Контент-движок, Фаза 5: чип «Связано с программой» ──────────────────
|
||||||
|
Подтягивает курикулумные связи симуляции (GET /api/lab/sims/:id/related) и
|
||||||
|
рендерит чипы-ссылки рядом с заголовком симуляции. Самодостаточно: создаёт
|
||||||
|
контейнер #sim-related динамически (без правок lab.html/CSS — меньше риск
|
||||||
|
конфликта с параллельными сессиями). Тихо прячется, если связей нет/ошибка. */
|
||||||
|
var _LAB_LINK_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
||||||
|
function _labRelEsc(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||||
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _ensureRelatedHost() {
|
||||||
|
var host = document.getElementById('sim-related');
|
||||||
|
if (host) return host;
|
||||||
|
host = document.createElement('div');
|
||||||
|
host.id = 'sim-related';
|
||||||
|
host.style.cssText = 'display:none;align-items:center;gap:6px;flex-wrap:wrap;margin-left:14px;min-width:0';
|
||||||
|
var title = document.getElementById('sim-topbar-title');
|
||||||
|
if (title && title.parentNode) title.parentNode.insertBefore(host, title.nextSibling);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
function _loadRelated(simId) {
|
||||||
|
var host = _ensureRelatedHost();
|
||||||
|
host.style.display = 'none';
|
||||||
|
host.innerHTML = '';
|
||||||
|
if (!window.LS || !LS.api) return;
|
||||||
|
LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related')
|
||||||
|
.then(function (data) {
|
||||||
|
var links = (data && data.links) || {};
|
||||||
|
var all = [].concat(links.textbook || [], links.topic || [], links.kmap || [], links.question || []);
|
||||||
|
if (!all.length) return;
|
||||||
|
var chipBase = 'display:inline-flex;align-items:center;gap:4px;font-size:.72rem;padding:3px 9px;border-radius:999px;';
|
||||||
|
var html = '<span style="font-size:.68rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.05em">'
|
||||||
|
+ _LAB_LINK_ICON + ' Связано с программой</span>';
|
||||||
|
all.forEach(function (l) {
|
||||||
|
var label = _labRelEsc(l.label || (l.kind + ':' + l.ref_id));
|
||||||
|
if (l.href) {
|
||||||
|
html += '<a href="' + _labRelEsc(l.href) + '" title="Открыть в учебнике" style="' + chipBase
|
||||||
|
+ 'background:rgba(155,93,229,.14);color:var(--violet);text-decoration:none;border:1px solid rgba(155,93,229,.32)">' + label + '</a>';
|
||||||
|
} else {
|
||||||
|
html += '<span style="' + chipBase
|
||||||
|
+ 'background:rgba(255,255,255,.06);color:var(--text-2);border:1px solid rgba(255,255,255,.12)">' + label + '</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
host.innerHTML = html;
|
||||||
|
host.style.display = 'flex';
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
})
|
||||||
|
.catch(function () { /* нет связей или ошибка — чип просто не показываем */ });
|
||||||
|
}
|
||||||
|
window._loadRelated = _loadRelated;
|
||||||
|
|
||||||
/* ── embed mode + auto-open from ?sim= ── */
|
/* ── embed mode + auto-open from ?sim= ── */
|
||||||
const _qp = new URLSearchParams(location.search);
|
const _qp = new URLSearchParams(location.search);
|
||||||
var _embedMode = _qp.get('embed') === '1';
|
var _embedMode = _qp.get('embed') === '1';
|
||||||
|
|||||||
@@ -30,6 +30,19 @@
|
|||||||
var geomSim = null;
|
var geomSim = null;
|
||||||
var qualSim = null;
|
var qualSim = null;
|
||||||
|
|
||||||
|
/* Контент-движок, Фаза 3 (ленивая загрузка): часть глобалов с экземплярами
|
||||||
|
симуляций объявляется внутри их собственных НЫНЕ ЛЕНИВЫХ файлов, поэтому до
|
||||||
|
первого открытия такой симуляции они не существуют. Legacy-«дробовик»
|
||||||
|
_pauseAllSims()/closeSim() ссылается на них по голому имени, что до загрузки
|
||||||
|
любого файла бросало ReferenceError (напр. cirSim). Предсоздаём эти имена как
|
||||||
|
свойства window (null), чтобы guard'ы безопасно давали false; при загрузке
|
||||||
|
файла симуляции его собственный var/присваивание обновит тот же глобал. */
|
||||||
|
['cirSim','reacSim','flaskSim','newtonSim','sandboxSim','crystalSim','orbitalsSim',
|
||||||
|
'stereoSim','angryBirdsSim','trigSim','pendSim','radioactiveSim','heSim',
|
||||||
|
'periodicSim','organicSim','_solutionsSim','mirrorSim'].forEach(function (_n) {
|
||||||
|
if (!(_n in window)) window[_n] = null;
|
||||||
|
});
|
||||||
|
|
||||||
var ALL_SIM_BODIES = ['sim-graph','sim-proj','sim-coll','sim-tri','sim-trigcircle','sim-emfield',
|
var ALL_SIM_BODIES = ['sim-graph','sim-proj','sim-coll','sim-tri','sim-trigcircle','sim-emfield',
|
||||||
'sim-molphys',
|
'sim-molphys',
|
||||||
'sim-circuit','sim-chemistry','sim-dynamics',
|
'sim-circuit','sim-chemistry','sim-dynamics',
|
||||||
@@ -52,6 +65,7 @@
|
|||||||
// Pause all animation-loop sims (non-destructive). Called when switching
|
// Pause all animation-loop sims (non-destructive). Called when switching
|
||||||
// between sims so a previously opened sim doesn't keep rendering offscreen.
|
// between sims so a previously opened sim doesn't keep rendering offscreen.
|
||||||
function _pauseAllSims() {
|
function _pauseAllSims() {
|
||||||
|
if (window.LabRegistry) window.LabRegistry.stopActive();
|
||||||
if (pSim) pSim.pause();
|
if (pSim) pSim.pause();
|
||||||
if (cSim) cSim.pause();
|
if (cSim) cSim.pause();
|
||||||
if (gasSim) gasSim.stop();
|
if (gasSim) gasSim.stop();
|
||||||
@@ -105,58 +119,34 @@
|
|||||||
// load theory for this sim
|
// load theory for this sim
|
||||||
loadTheory(id.includes(':') ? id.split(':')[0] : id);
|
loadTheory(id.includes(':') ? id.split(':')[0] : id);
|
||||||
|
|
||||||
if (id === 'graph') _openGraph();
|
// Фаза 5: чип «Связано с программой» (курикулумные связи симуляции).
|
||||||
if (id === 'projectile') _openProjectile();
|
if (typeof _loadRelated === 'function') _loadRelated(id.includes(':') ? id.split(':')[0] : id);
|
||||||
if (id === 'collision') _openCollision();
|
|
||||||
if (id === 'triangle') _openTriangle();
|
// ── Контент-движок (Фаза 1): диспетчеризация через реестр ──
|
||||||
if (id === 'trigcircle') _openTrigCircle();
|
// Все каталожные симуляции зарегистрированы в _register-all.js.
|
||||||
if (id === 'magnetic') _openEMField('B'); // backward compat: #magnetic → emfield B-mode
|
// Алиасы deep-link (magnetic/coulomb/thinlens/mirrors/refraction) нормализуем
|
||||||
if (id === 'coulomb') _openEMField('E'); // backward compat: #coulomb → emfield E-mode
|
// в канонический id[:arg] перед обращением к реестру.
|
||||||
if (id === 'emfield') _openEMField('E');
|
var _aliases = window.LAB_SIM_ALIASES || {};
|
||||||
if (id.startsWith('emfield:')) { _openEMField(id.split(':')[1]); }
|
var _cid = _aliases[id.split(':')[0]] || id;
|
||||||
if (id === 'molphys') _openMolPhys();
|
if (window.LabRegistry && window.LabRegistry.has(_cid)) {
|
||||||
if (id.startsWith('molphys:')) { _openMolPhys(id.split(':')[1]); }
|
const _m = window.LabRegistry.get(_cid);
|
||||||
if (id === 'circuit') _openCircuit();
|
const _arg = _cid.includes(':') ? _cid.split(':')[1] : undefined;
|
||||||
if (id === 'chemistry') _openChemistry();
|
window.LabRegistry.setActive(_m);
|
||||||
if (id.startsWith('chemistry:')) { _openChemistry(id.split(':')[1]); }
|
// Фаза 3: open() может вернуть Promise (ленивая загрузка кода). Иконки
|
||||||
if (id === 'dynamics') _openDynamics();
|
// перерисовываем после фактической инициализации тела симуляции; ошибку
|
||||||
if (id.startsWith('dynamics:')) { _openDynamics(id.split(':')[1]); }
|
// асинхронной загрузки ловим через .catch (sync try/catch её не поймает).
|
||||||
if (id === 'crystal') _openCrystal();
|
try {
|
||||||
if (id === 'orbitals') _openOrbitals();
|
const _r = _m.open({ id: _cid, arg: _arg });
|
||||||
if (id === 'stereo') _openStereo();
|
if (_r && typeof _r.then === 'function') {
|
||||||
if (id.startsWith('stereo:')) { _openStereo(id.split(':')[1]); }
|
_r.then(function () { if (window.lucide) lucide.createIcons(); })
|
||||||
if (id === 'chemsandbox') _openChemSandbox();
|
.catch(function (e) { console.error('[LabRegistry] open failed:', _cid, e); });
|
||||||
if (id === 'celldivision') _openCellDivision();
|
} else if (window.lucide) {
|
||||||
if (id === 'photosynthesis') _openPhotosynthesis();
|
lucide.createIcons();
|
||||||
if (id === 'angrybirds') _openAngryBirds();
|
}
|
||||||
if (id === 'quadratic') _openQuadratic();
|
} catch (e) { console.error('[LabRegistry] open failed:', _cid, e); }
|
||||||
if (id === 'normaldist') _openNormalDist();
|
return;
|
||||||
if (id === 'graphtransform') _openGraphTransform();
|
}
|
||||||
if (id === 'pendulum') _openPendulum();
|
if (window.console) console.warn('[LabRegistry] неизвестная симуляция:', id);
|
||||||
if (id === 'equilibrium') _openEquilibrium();
|
|
||||||
if (id === 'opticsbench') _openOpticsBench('lens');
|
|
||||||
if (id.startsWith('opticsbench:')) _openOpticsBench(id.split(':')[1]);
|
|
||||||
if (id === 'thinlens') _openOpticsBench('lens'); // backward compat
|
|
||||||
if (id === 'mirrors') _openOpticsBench('mirror'); // backward compat
|
|
||||||
if (id === 'refraction') _openOpticsBench('refraction'); // backward compat
|
|
||||||
if (id === 'isoprocess') _openIsoprocess();
|
|
||||||
if (id === 'titration') _openTitration();
|
|
||||||
if (id === 'probability') _openProbability();
|
|
||||||
if (id === 'bohratom') _openBohrAtom();
|
|
||||||
if (id === 'electrolysis') _openElectrolysis();
|
|
||||||
if (id === 'race') _openRace();
|
|
||||||
if (id === 'waves') _openWaves();
|
|
||||||
if (id === 'hydrostatics') _openHydro();
|
|
||||||
if (id.startsWith('hydrostatics:')) _openHydro(id.split(':')[1]);
|
|
||||||
if (id === 'radioactive') _openRadioactive();
|
|
||||||
if (id === 'geometry') _openGeometry();
|
|
||||||
if (id === 'logic') _openLogic();
|
|
||||||
if (id === 'heatengine') _openHeatEngine();
|
|
||||||
if (id === 'stoichiometry') _openStoich();
|
|
||||||
if (id === 'qualanalysis') _openQualAnalysis();
|
|
||||||
if (id === 'periodic') _openPeriodic();
|
|
||||||
if (id === 'organic') _openOrganic();
|
|
||||||
if (id === 'solutions') _openSolutions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _simShow(elId) {
|
function _simShow(elId) {
|
||||||
@@ -210,6 +200,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeSim() {
|
function closeSim() {
|
||||||
|
if (window.LabRegistry) window.LabRegistry.destroyActive();
|
||||||
if (pSim) pSim.pause();
|
if (pSim) pSim.pause();
|
||||||
if (cSim) cSim.pause();
|
if (cSim) cSim.pause();
|
||||||
if (mSim && mSim.particleOn) mSim.toggleParticle();
|
if (mSim && mSim.particleOn) mSim.toggleParticle();
|
||||||
|
|||||||
+39
-4475
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+17
-1
@@ -454,16 +454,28 @@
|
|||||||
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let labLinks = {}; // { textbook_slug: [{id,title,cat}] } — связанные симуляции (Фаза 5)
|
||||||
async function loadTextbooks() {
|
async function loadTextbooks() {
|
||||||
try {
|
try {
|
||||||
const r = await LS.api('/api/textbooks');
|
const [r, labRes] = await Promise.all([
|
||||||
|
LS.api('/api/textbooks'),
|
||||||
|
LS.api('/api/lab/links/all?kind=textbook').catch(() => ({ byRef: {} })),
|
||||||
|
]);
|
||||||
textbooks = r.textbooks || [];
|
textbooks = r.textbooks || [];
|
||||||
|
labLinks = (labRes && labRes.byRef) || {};
|
||||||
render();
|
render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('tb-grid').innerHTML = `<div class="tb-empty">Не удалось загрузить: ${esc(e.message)}</div>`;
|
document.getElementById('tb-grid').innerHTML = `<div class="tb-empty">Не удалось загрузить: ${esc(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Фаза 5: открыть связанную симуляцию из карточки учебника (не уходя в учебник). */
|
||||||
|
function openLabSim(simId, ev) {
|
||||||
|
if (ev) ev.stopPropagation();
|
||||||
|
location.href = '/lab?sim=' + encodeURIComponent(simId);
|
||||||
|
}
|
||||||
|
window.openLabSim = openLabSim;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const grid = document.getElementById('tb-grid');
|
const grid = document.getElementById('tb-grid');
|
||||||
if (!textbooks.length) {
|
if (!textbooks.length) {
|
||||||
@@ -509,6 +521,10 @@
|
|||||||
${isTeacher ? `<button class="tb-btn tb-assign-btn" onclick="openAssignModal('${t.slug}', '${esc(t.title)}')" title="Назначить чтение как ДЗ">
|
${isTeacher ? `<button class="tb-btn tb-assign-btn" onclick="openAssignModal('${t.slug}', '${esc(t.title)}')" title="Назначить чтение как ДЗ">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M2 12h20"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M2 12h20"/></svg>
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
|
${(labLinks[t.slug] && labLinks[t.slug].length) ? `<button class="tb-btn tb-lab-btn" onclick="openLabSim('${esc(labLinks[t.slug][0].id)}', event)" title="Открыть связанную симуляцию в лаборатории">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg>
|
||||||
|
В лабораторию${labLinks[t.slug].length > 1 ? ' (' + labLinks[t.slug].length + ')' : ''}
|
||||||
|
</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>`;
|
</article>`;
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 1 · «Важнейшие классы неорганических соединений»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 1 — teal-палитра */
|
||||||
|
:root{ --pri:#0d9488; --pri-d:#0f766e; --pri-l:#2dd4bf; --pri-soft:#ccfbf1; --sec-acc:#0d9488; --sec-acc-d:#0f766e; --sec-acc-soft:#ccfbf1; }
|
||||||
|
html.dark{ --bg:#0c1a18; --card:#102825; --card-soft:#13302c; --text:#ccfbf1; --muted:#7fd8c8; --border:#1d463f; --pri-soft:rgba(13,148,136,.2); --sec-acc-soft:rgba(13,148,136,.2); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#134e4a 0%,#0d9488 55%,#2dd4bf 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 1'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch1_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 1</h1>
|
||||||
|
<div class="hdr-sub">Оксиды, кислоты, основания и соли: состав, классификация, свойства, получение и генетическая связь</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Четыре класса, из которых построена неорганическая химия</h2>
|
||||||
|
<p>Оксиды, кислоты, основания и соли связаны между собой превращениями. Научившись узнавать класс вещества по формуле и предсказывать его реакции, ты сможешь «читать» химию как язык.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p10')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 10</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p10" class="sec"><div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Оксиды. Состав и классификация</h2></div><div id="p10-body"></div></section>
|
||||||
|
<section id="sec-p11" class="sec"><div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Химические свойства оксидов</h2></div><div id="p11-body"></div></section>
|
||||||
|
<section id="sec-p12" class="sec"><div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Получение и применение оксидов</h2></div><div id="p12-body"></div></section>
|
||||||
|
<section id="sec-p13" class="sec"><div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Кислоты. Состав и классификация</h2></div><div id="p13-body"></div></section>
|
||||||
|
<section id="sec-p14" class="sec"><div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Химические свойства кислот</h2></div><div id="p14-body"></div></section>
|
||||||
|
<section id="sec-p15" class="sec"><div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Получение и применение кислот</h2></div><div id="p15-body"></div></section>
|
||||||
|
<section id="sec-p16" class="sec"><div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Основания</h2></div><div id="p16-body"></div></section>
|
||||||
|
<section id="sec-p17" class="sec"><div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Химические свойства оснований</h2></div><div id="p17-body"></div></section>
|
||||||
|
<section id="sec-p18" class="sec"><div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Получение оснований · Лаб. 1 · ПР 2</h2></div><div id="p18-body"></div></section>
|
||||||
|
<section id="sec-p19" class="sec"><div class="sec-header"><span class="sec-num">§ 19</span><h2 class="sec-h">Соли. Состав и классификация</h2></div><div id="p19-body"></div></section>
|
||||||
|
<section id="sec-p20" class="sec"><div class="sec-header"><span class="sec-num">§ 20</span><h2 class="sec-h">Химические свойства солей · Лаб. 2</h2></div><div id="p20-body"></div></section>
|
||||||
|
<section id="sec-p21" class="sec"><div class="sec-header"><span class="sec-num">§ 21</span><h2 class="sec-h">Получение и применение солей</h2></div><div id="p21-body"></div></section>
|
||||||
|
<section id="sec-p22" class="sec"><div class="sec-header"><span class="sec-num">§ 22</span><h2 class="sec-h">Взаимосвязь классов · ПР 3</h2></div><div id="p22-body"></div></section>
|
||||||
|
<section id="sec-p23" class="sec"><div class="sec-header"><span class="sec-num">§ 23</span><h2 class="sec-h">Решение расчётных задач</h2></div><div id="p23-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 1 · «Важнейшие классы неорганических соединений» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch1', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch1_progress', achKey:'chemistry8_ch1_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p10',num:'§ 10',name:'Оксиды. Классификация',sub:'осн/кисл/амф'},
|
||||||
|
{id:'p11',num:'§ 11',name:'Свойства оксидов',sub:'+ вода/кислота/щёлочь'},
|
||||||
|
{id:'p12',num:'§ 12',name:'Получение оксидов',sub:'горение · разложение'},
|
||||||
|
{id:'p13',num:'§ 13',name:'Кислоты. Классификация',sub:'основность'},
|
||||||
|
{id:'p14',num:'§ 14',name:'Свойства кислот',sub:'+ Me · ряд активности'},
|
||||||
|
{id:'p15',num:'§ 15',name:'Получение кислот',sub:'оксид + вода'},
|
||||||
|
{id:'p16',num:'§ 16',name:'Основания',sub:'щёлочи/нераств.'},
|
||||||
|
{id:'p17',num:'§ 17',name:'Свойства оснований',sub:'нейтрализация'},
|
||||||
|
{id:'p18',num:'§ 18',name:'Получение · Лаб.1 · ПР2',sub:'Cu(OH)₂↓'},
|
||||||
|
{id:'p19',num:'§ 19',name:'Соли. Классификация',sub:'растворимость'},
|
||||||
|
{id:'p20',num:'§ 20',name:'Свойства солей · Лаб.2',sub:'РИО · соль+Me'},
|
||||||
|
{id:'p21',num:'§ 21',name:'Получение солей',sub:'способы'},
|
||||||
|
{id:'p22',num:'§ 22',name:'Связь классов · ПР3',sub:'генетика'},
|
||||||
|
{id:'p23',num:'§ 23',name:'Расчётные задачи',sub:'по уравнениям'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 1!', final1_tasks:'Классы веществ покорены!' };
|
||||||
|
['p10','p11','p12','p13','p14','p15','p16','p17','p18','p19','p20','p21','p22','p23'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p10:{title:'§10 Оксиды',rows:[['Оксид','ЭₓOᵧ, кислород −2'],['Основные','оксиды металлов'],['Кислотные','оксиды неметаллов'],['Амфотерные','ZnO, Al₂O₃']]},
|
||||||
|
p11:{title:'§11 Свойства',rows:[['Осн. оксид','+ кислота, + вода'],['Кисл. оксид','+ щёлочь, + вода'],['Соль','продукт + H₂O']]},
|
||||||
|
p12:{title:'§12 Получение',rows:[['Горение','S + O₂ → SO₂'],['Разложение','CaCO₃ → CaO + CO₂']]},
|
||||||
|
p13:{title:'§13 Кислоты',rows:[['Кислота','HₓAc'],['Бескислородные','HCl, H₂S'],['Кислородсод.','H₂SO₄, HNO₃'],['Основность','число H']]},
|
||||||
|
p14:{title:'§14 Свойства кислот',rows:[['+ Me','до H в ряду → H₂↑'],['+ осн. оксид','соль + вода'],['+ основание','нейтрализация'],['Индикатор','лакмус → красный']]},
|
||||||
|
p15:{title:'§15 Получение',rows:[['Кисл. оксид + вода','SO₃+H₂O→H₂SO₄'],['Соль + кислота','']]},
|
||||||
|
p16:{title:'§16 Основания',rows:[['Основание','Me(OH)ₙ'],['Щёлочи','NaOH, KOH, Ba(OH)₂'],['Нерастворимые','Cu(OH)₂, Fe(OH)₃'],['Фенолфталеин','малиновый в щёлочи']]},
|
||||||
|
p17:{title:'§17 Свойства',rows:[['Нейтрализация','+ кислота → соль+H₂O'],['+ кисл. оксид','соль + вода'],['Разложение','Cu(OH)₂ → CuO+H₂O']]},
|
||||||
|
p18:{title:'§18 Получение',rows:[['Щёлочь','Me + вода'],['Нераств.','щёлочь + соль'],['Лаб.1','Cu(OH)₂↓ голубой']]},
|
||||||
|
p19:{title:'§19 Соли',rows:[['Соль','катион металла + анион'],['Средние','NaCl, CaCO₃'],['Растворимость','таблица']]},
|
||||||
|
p20:{title:'§20 Свойства',rows:[['РИО','↓ ↑ H₂O'],['Соль + Me','активнее вытесняет'],['Лаб.2','соль + металл']]},
|
||||||
|
p21:{title:'§21 Получение',rows:[['Кислота+основание',''],['Металл+кислота',''],['Оксид+кислота','']]},
|
||||||
|
p22:{title:'§22 Связь',rows:[['Металл','→ оксид → основание → соль'],['Неметалл','→ оксид → кислота → соль']]},
|
||||||
|
p23:{title:'§23 Расчёты',rows:[['Алгоритм','m→n→n→m'],['По уравнению','мольное отношение']]},
|
||||||
|
final1:{title:'Финал главы 1',rows:[['§§10–23','все классы'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p10',html:'Оксид — соединение элемента с кислородом (степень окисления −2). Оксиды металлов чаще основные, неметаллов — кислотные.'},
|
||||||
|
{sec:'p11',html:'Основный оксид реагирует с кислотой, кислотный — со щёлочью. Оба могут давать соль.'},
|
||||||
|
{sec:'p12',html:'Оксиды получают горением простых веществ и разложением солей/оснований.'},
|
||||||
|
{sec:'p13',html:'Основность кислоты = число атомов H, способных замещаться металлом (HCl — одноосновная, H₂SO₄ — двухосновная).'},
|
||||||
|
{sec:'p14',html:'Металлы ДО водорода в ряду активности вытесняют H₂ из растворов кислот; после — нет.'},
|
||||||
|
{sec:'p15',html:'Кислородсодержащие кислоты получают: кислотный оксид + вода. Например, SO₃ + H₂O → H₂SO₄.'},
|
||||||
|
{sec:'p16',html:'Растворимые основания — щёлочи (NaOH, KOH, Ba(OH)₂). Фенолфталеин в щёлочи становится малиновым.'},
|
||||||
|
{sec:'p17',html:'Реакция нейтрализации: основание + кислота → соль + вода.'},
|
||||||
|
{sec:'p18',html:'Нерастворимое основание получают: щёлочь + соль. Cu(OH)₂ — голубой осадок.'},
|
||||||
|
{sec:'p19',html:'Растворимость соли определяют по таблице. Нерастворимая соль выпадает в осадок.'},
|
||||||
|
{sec:'p20',html:'Реакции ионного обмена идут, если образуется осадок ↓, газ ↑ или вода. Металл вытесняет из соли менее активный металл.'},
|
||||||
|
{sec:'p21',html:'Соли получают многими способами: кислота+основание, металл+кислота, оксид+кислота, соль+соль и др.'},
|
||||||
|
{sec:'p22',html:'Генетическая связь: металл → основный оксид → основание → соль; неметалл → кислотный оксид → кислота → соль.'},
|
||||||
|
{sec:'p23',html:'Расчёт по уравнению: найди n известного, по коэффициентам — n искомого, переведи в массу/объём.'},
|
||||||
|
{sec:'final1',html:'Собери всё: классификация, свойства, растворимость, ряд активности, расчёты по уравнениям.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p10:[
|
||||||
|
{q:'Оксид — это…',opts:['Соединение двух неметаллов','Сложное вещество из двух элементов, один из которых кислород (с.о. −2)','Любое вещество с кислородом','Соль кислородной кислоты'],a:1,ex:'Оксид — бинарное соединение элемента с кислородом.'},
|
||||||
|
{q:'Какой из оксидов основный?',opts:['CO₂','SO₃','Na₂O','P₂O₅'],a:2,ex:'Na₂O — оксид активного металла, основный.'},
|
||||||
|
{q:'Амфотерный оксид — это…',opts:['Na₂O','CaO','ZnO','CO'],a:2,ex:'ZnO проявляет и основные, и кислотные свойства.'},
|
||||||
|
{q:'Несолеобразующий оксид:',opts:['CO','SO₂','CaO','Al₂O₃'],a:0,ex:'CO не образует солей — несолеобразующий.'}
|
||||||
|
],
|
||||||
|
p11:[
|
||||||
|
{q:'Основный оксид реагирует с…',opts:['Основанием','Кислотой','Другим основным оксидом','Инертным газом'],a:1,ex:'Основный оксид + кислота → соль + вода.'},
|
||||||
|
{q:'Кислотный оксид реагирует с…',opts:['Кислотой','Щёлочью','Кислотным оксидом','Металлом'],a:1,ex:'Кислотный оксид + щёлочь → соль + вода.'},
|
||||||
|
{q:'Продукт реакции CaO + H₂O:',opts:['CaCO₃','Ca(OH)₂','CaCl₂','CaO₂'],a:1,ex:'Основный оксид активного металла + вода → щёлочь.'}
|
||||||
|
],
|
||||||
|
p12:[
|
||||||
|
{q:'Каким способом получают оксиды?',opts:['Только электролизом','Горением простых веществ и разложением солей','Только из кислот','Только из щелочей'],a:1,ex:'Горение (S+O₂→SO₂) и разложение (CaCO₃→CaO+CO₂).'},
|
||||||
|
{q:'При разложении 1 моль CaCO₃ образуется … моль CO₂',hint:'1:1',unit:'моль',a:1,ex:'CaCO₃ → CaO + CO₂, отношение 1:1.'}
|
||||||
|
],
|
||||||
|
p13:[
|
||||||
|
{q:'Бескислородная кислота — это…',opts:['H₂SO₄','HNO₃','HCl','H₃PO₄'],a:2,ex:'HCl не содержит кислорода.'},
|
||||||
|
{q:'Основность серной кислоты H₂SO₄ равна…',hint:'число атомов H',unit:'',a:2,ex:'Два атома водорода → двухосновная.'},
|
||||||
|
{q:'Какого цвета лакмус в растворе кислоты?',opts:['Синий','Красный','Жёлтый','Бесцветный'],a:1,ex:'Лакмус в кислоте — красный.'}
|
||||||
|
],
|
||||||
|
p14:[
|
||||||
|
{q:'Какой металл вытесняет водород из соляной кислоты?',opts:['Cu','Ag','Zn','Au'],a:2,ex:'Zn стоит до H в ряду активности.'},
|
||||||
|
{q:'Реакция кислоты с основанием называется…',opts:['Разложения','Нейтрализации','Замещения','Горения'],a:1,ex:'Кислота + основание → соль + вода — нейтрализация.'},
|
||||||
|
{q:'Медь (Cu) с разбавленной HCl…',opts:['Бурно реагирует','Не реагирует (стоит после H)','Образует H₂','Растворяется с осадком'],a:1,ex:'Cu после водорода — H₂ не вытесняет.'}
|
||||||
|
],
|
||||||
|
p15:[
|
||||||
|
{q:'H₂SO₄ можно получить реакцией…',opts:['SO₃ + H₂O','SO₂ + H₂','S + H₂O','Na₂SO₄ + H₂O'],a:0,ex:'Кислотный оксид SO₃ + вода → серная кислота.'},
|
||||||
|
{q:'Какой оксид при растворении в воде даёт кислоту?',opts:['CaO','Na₂O','CO₂','MgO'],a:2,ex:'CO₂ + H₂O → H₂CO₃ (кислотный оксид).'}
|
||||||
|
],
|
||||||
|
p16:[
|
||||||
|
{q:'Щёлочь — это…',opts:['Любое основание','Растворимое в воде основание','Нерастворимое основание','Кислота'],a:1,ex:'Щёлочи — растворимые основания (NaOH, KOH...).'},
|
||||||
|
{q:'Нерастворимое основание:',opts:['NaOH','KOH','Cu(OH)₂','Ba(OH)₂'],a:2,ex:'Cu(OH)₂ нерастворим.'},
|
||||||
|
{q:'Фенолфталеин в растворе щёлочи становится…',opts:['Красным','Малиновым','Жёлтым','Синим'],a:1,ex:'В щёлочи фенолфталеин малиновый.'}
|
||||||
|
],
|
||||||
|
p17:[
|
||||||
|
{q:'Продукты реакции NaOH + HCl:',opts:['NaCl + H₂','NaCl + H₂O','Na₂O + HCl','NaH + ClOH'],a:1,ex:'Нейтрализация: NaCl + H₂O.'},
|
||||||
|
{q:'При нагревании Cu(OH)₂ разлагается на…',opts:['Cu + H₂O','CuO + H₂O','CuO + H₂','Cu + O₂'],a:1,ex:'Cu(OH)₂ → CuO + H₂O.'}
|
||||||
|
],
|
||||||
|
p18:[
|
||||||
|
{q:'Cu(OH)₂ в лаборатории получают реакцией…',opts:['Cu + H₂O','CuSO₄ + NaOH','CuO + H₂O','Cu + NaOH'],a:1,ex:'Соль + щёлочь → нерастворимое основание Cu(OH)₂↓.'},
|
||||||
|
{q:'Какого цвета осадок Cu(OH)₂?',opts:['Белый','Голубой','Бурый','Чёрный'],a:1,ex:'Гидроксид меди(II) — голубой осадок.'}
|
||||||
|
],
|
||||||
|
p19:[
|
||||||
|
{q:'Соль — это…',opts:['Оксид металла','Продукт замещения H кислоты на металл','Гидроксид','Простое вещество'],a:1,ex:'В соли водород кислоты замещён металлом.'},
|
||||||
|
{q:'Какая соль нерастворима в воде?',opts:['NaCl','KNO₃','BaSO₄','CaCl₂'],a:2,ex:'BaSO₄ — нерастворим (см. таблицу).'},
|
||||||
|
{q:'Все нитраты (NO₃⁻)…',opts:['Нерастворимы','Растворимы','Малорастворимы','Разлагаются водой'],a:1,ex:'Нитраты растворимы все.'}
|
||||||
|
],
|
||||||
|
p20:[
|
||||||
|
{q:'Реакция ионного обмена идёт, если образуется…',opts:['Только газ','Осадок ↓, газ ↑ или вода','Только осадок','Новая кислота'],a:1,ex:'Условие необратимости РИО.'},
|
||||||
|
{q:'Какой металл вытеснит медь из раствора CuSO₄?',opts:['Ag','Au','Fe','Hg'],a:2,ex:'Fe активнее Cu → вытесняет медь.'},
|
||||||
|
{q:'Реакция AgNO₃ + NaCl даёт осадок…',opts:['AgNO₃','NaCl','AgCl↓','NaNO₃'],a:2,ex:'AgCl — белый осадок ↓.'}
|
||||||
|
],
|
||||||
|
p21:[
|
||||||
|
{q:'Какой способ НЕ даёт соль?',opts:['Кислота + основание','Металл + кислота','Оксид + кислота','Два инертных газа'],a:3,ex:'Инертные газы не реагируют.'},
|
||||||
|
{q:'Zn + 2HCl даёт соль…',opts:['ZnO','ZnCl₂','Zn(OH)₂','ZnSO₄'],a:1,ex:'ZnCl₂ + H₂↑.'}
|
||||||
|
],
|
||||||
|
p22:[
|
||||||
|
{q:'Генетический ряд металла:',opts:['Me → кислота → соль','Me → основный оксид → основание → соль','Me → кислотный оксид → кислота','Me → соль → оксид'],a:1,ex:'Металл → осн. оксид → основание → соль.'},
|
||||||
|
{q:'Из чего можно получить кислоту в ряду неметалла?',opts:['Из основного оксида','Из кислотного оксида + вода','Из металла','Из щёлочи'],a:1,ex:'Кислотный оксид + вода → кислота.'}
|
||||||
|
],
|
||||||
|
p23:[
|
||||||
|
{q:'CaO + 2HCl → CaCl₂ + H₂O. Дано n(CaO)=0,5 моль. Найди n(CaCl₂).',hint:'1:1',unit:'моль',a:0.5,ex:'Отношение 1:1 → 0,5 моль.'},
|
||||||
|
{q:'Zn + H₂SO₄ → ZnSO₄ + H₂↑. Дано n(Zn)=2 моль. Найди V(H₂) при н.у.',hint:'n(H₂)=2; V=n·22,4',unit:'л',a:44.8,ex:'2·22,4=44,8 л.'},
|
||||||
|
{q:'2NaOH + H₂SO₄ → Na₂SO₄ + 2H₂O. Дано n(H₂SO₄)=0,5 моль. Найди n(NaOH).',hint:'2:1',unit:'моль',a:1,ex:'2·0,5=1 моль.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'Какой оксид кислотный?',opts:['CaO','Na₂O','SO₃','MgO'],a:2,ex:'SO₃ — оксид неметалла, кислотный.'},
|
||||||
|
{q:'Основность H₃PO₄ равна…',hint:'число H',unit:'',a:3,ex:'Три атома H — трёхосновная.'},
|
||||||
|
{q:'Какой металл НЕ вытесняет H₂ из кислоты?',opts:['Mg','Zn','Fe','Cu'],a:3,ex:'Cu стоит после H.'},
|
||||||
|
{q:'Реакция CuSO₄ + 2NaOH даёт осадок…',opts:['CuO','Cu(OH)₂↓','Na₂SO₄','Cu'],a:1,ex:'Cu(OH)₂ — голубой осадок ↓.'},
|
||||||
|
{q:'Дано: m(CaCO₃)=50 г, M=100. Найди n.',hint:'n=m/M',unit:'моль',a:0.5,ex:'50/100=0,5 моль.'},
|
||||||
|
{q:'Zn + 2HCl → ZnCl₂ + H₂↑. n(Zn)=1 моль. Найди V(H₂) н.у.',hint:'V=n·22,4',unit:'л',a:22.4,ex:'1·22,4=22,4 л.'},
|
||||||
|
{q:'Mr(Cu(OH)₂)?',hint:'64+2·(16+1)',unit:'',a:98,ex:'64+34=98.'},
|
||||||
|
{q:'Какого цвета осадок Fe(OH)₃?',opts:['Голубой','Бурый','Белый','Чёрный'],a:1,ex:'Fe(OH)₃ — бурый.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/* конструкторы */
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function wgt(title, inner){ return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
function tube(o){ return (window.Chem8&&window.Chem8.testTube)?window.Chem8.testTube(o):''; }
|
||||||
|
|
||||||
|
window.BUILDERS = {
|
||||||
|
p10:bp10,p11:bp11,p12:bp12,p13:bp13,p14:bp14,p15:bp15,p16:bp16,p17:bp17,p18:bp18,p19:bp19,p20:bp20,p21:bp21,p22:bp22,p23:bp23,final1:bfinal
|
||||||
|
};
|
||||||
|
|
||||||
|
function bp10(){ document.getElementById('p10-body').innerHTML =
|
||||||
|
hero(1,'§ 10 · Глава 1','Оксиды. Состав и классификация','ЭₓOᵧ','Самый распространённый класс: соединения элементов с кислородом — от песка и воды до углекислого газа.',['оксид','классификация','амфотерность'])
|
||||||
|
+makeCard('theory','Что такое оксид','§10','<p><b>Оксид</b> — сложное вещество из двух элементов, один из которых — кислород в степени окисления −2 (Na₂O, CaO, CO₂, SO₃, H₂O).</p><p>Оксиды делят на <b>солеобразующие</b> (дают соли) и <b>несолеобразующие</b> (CO, N₂O, NO). Солеобразующие — на <b>основные</b> (оксиды металлов: Na₂O, CaO), <b>кислотные</b> (оксиды неметаллов: CO₂, SO₃, P₂O₅) и <b>амфотерные</b> (ZnO, Al₂O₃).</p>')
|
||||||
|
+flag('Классификатор оксидов','Перетащи (кликни чип, затем корзину) каждый оксид в свой класс и нажми «Проверить».','<div id="c-ox-cls"></div>')
|
||||||
|
+rememberBox(['Оксиды металлов чаще основные, неметаллов — кислотные.','Амфотерные: ZnO, Al₂O₃, Cr₂O₃ — ведут себя двойственно.','CO, NO, N₂O — несолеобразующие.'])
|
||||||
|
+qList(['Чем основный оксид отличается от кислотного?','Приведи по два примера оксидов каждого класса.'])
|
||||||
|
+secNav(null,'p11')+readButton('p10'); wireReadBtn('p10'); }
|
||||||
|
|
||||||
|
function bp11(){ document.getElementById('p11-body').innerHTML =
|
||||||
|
hero(2,'§ 11 · Глава 1','Химические свойства оксидов','оксид + … → соль','Как основные и кислотные оксиды реагируют с водой, кислотами и щелочами, образуя соли.',['+ вода','+ кислота','+ щёлочь'])
|
||||||
|
+makeCard('theory','Свойства оксидов','§11','<p><b>Основные оксиды:</b><br>• + вода (только активные металлы) → щёлочь: CaO + H₂O → Ca(OH)₂<br>• + кислота → соль + вода: CuO + 2HCl → CuCl₂ + H₂O</p><p><b>Кислотные оксиды:</b><br>• + вода → кислота: SO₃ + H₂O → H₂SO₄<br>• + щёлочь → соль + вода: CO₂ + 2NaOH → Na₂CO₃ + H₂O</p>')
|
||||||
|
+makeCard('example','Признак реакции',null,'<div class="tt-row"><div><div style="text-align:center">'+tube({color:'#dbeafe'})+'</div><div class="tt-cap">CuO + кислота → голубой раствор соли</div></div></div>')
|
||||||
|
+rememberBox(['Основный + кислотный оксид → соль (CaO + CO₂ → CaCO₃).','Не все оксиды реагируют с водой (CuO, Fe₂O₃ — нет).'])
|
||||||
|
+qList(['Допиши: SO₂ + NaOH → ?','С чем реагирует основный оксид CaO?'])
|
||||||
|
+secNav('p10','p12')+readButton('p11'); wireReadBtn('p11'); }
|
||||||
|
|
||||||
|
function bp12(){ document.getElementById('p12-body').innerHTML =
|
||||||
|
hero(3,'§ 12 · Глава 1','Получение и применение оксидов','S + O₂ → SO₂','Откуда берут оксиды и где их применяют — от извести до кремнезёма.',['горение','разложение','применение'])
|
||||||
|
+makeCard('theory','Способы получения','§12','<p>• <b>Горение</b> простых веществ: S + O₂ → SO₂; 2Cu + O₂ → 2CuO.<br>• <b>Разложение</b> солей и оснований: CaCO₃ →(t) CaO + CO₂↑; Cu(OH)₂ →(t) CuO + H₂O.</p><p><b>Применение:</b> CaO (негашёная известь) — в строительстве, SiO₂ — стекло, CO₂ — газировка и огнетушители.</p>')
|
||||||
|
+rememberBox(['Горение неметалла даёт кислотный оксид, металла — основный.','Карбонаты при нагревании разлагаются на оксид и CO₂.'])
|
||||||
|
+qList(['Напиши уравнение горения углерода.','Где применяют негашёную известь CaO?'])
|
||||||
|
+secNav('p11','p13')+readButton('p12'); wireReadBtn('p12'); }
|
||||||
|
|
||||||
|
function bp13(){ document.getElementById('p13-body').innerHTML =
|
||||||
|
hero(4,'§ 13 · Глава 1','Кислоты. Состав и классификация','HₓAc','Вещества с кислым вкусом и подвижными атомами водорода: как их различают и классифицируют.',['основность','индикатор'])
|
||||||
|
+makeCard('theory','Кислоты и их классификация','§13','<p><b>Кислота</b> — сложное вещество, в составе которого есть атомы водорода, способные замещаться металлом, и кислотный остаток.</p><p>По наличию кислорода: <b>бескислородные</b> (HCl, H₂S, HBr) и <b>кислородсодержащие</b> (H₂SO₄, HNO₃, H₃PO₄). По числу атомов H — <b>основность</b>: одноосновные (HCl), двухосновные (H₂SO₄), трёхосновные (H₃PO₄).</p>')
|
||||||
|
+flag('Классификатор кислот','Распредели кислоты по наличию кислорода.','<div id="c-acid-cls"></div>')
|
||||||
|
+wgt('Индикатор: кислая среда','<div id="c-acid-ind"></div>')
|
||||||
|
+rememberBox(['Основность = число замещаемых атомов H.','Лакмус в кислоте красный, метилоранж — розово-красный.'])
|
||||||
|
+qList(['Определи основность H₃PO₄.','Какие кислоты бескислородные?'])
|
||||||
|
+secNav('p12','p14')+readButton('p13'); wireReadBtn('p13'); }
|
||||||
|
|
||||||
|
function bp14(){ document.getElementById('p14-body').innerHTML =
|
||||||
|
hero(5,'§ 14 · Глава 1','Химические свойства кислот','+ Me · + оксид · + основание','Четыре главные реакции кислот и роль ряда активности металлов.',['ряд активности','нейтрализация'])
|
||||||
|
+makeCard('theory','Свойства кислот','§14','<p>Кислоты реагируют:<br>• с <b>металлами</b> (до H в ряду активности) → соль + H₂↑: Zn + 2HCl → ZnCl₂ + H₂↑<br>• с <b>основными оксидами</b> → соль + вода: CuO + H₂SO₄ → CuSO₄ + H₂O<br>• с <b>основаниями</b> (нейтрализация) → соль + вода<br>• с <b>солями</b> → новая соль + новая кислота (если ↓↑).</p>')
|
||||||
|
+flag('Ряд активности металлов','Кликни металл — узнаешь, вытесняет ли он водород из кислот и какие металлы вытесняет из солей.','<div id="c-acid-act"></div>')
|
||||||
|
+wgt('Индикатор: метилоранж','<div id="c-acid-ind2"></div>')
|
||||||
|
+rememberBox(['Металлы после H (Cu, Ag, Au) водород из кислот НЕ вытесняют.','Признак реакции с металлом — выделение пузырьков H₂↑.'])
|
||||||
|
+qList(['Будет ли реагировать Ag с HCl? Почему?','Допиши: Fe + HCl → ?'])
|
||||||
|
+secNav('p13','p15')+readButton('p14'); wireReadBtn('p14'); }
|
||||||
|
|
||||||
|
function bp15(){ document.getElementById('p15-body').innerHTML =
|
||||||
|
hero(6,'§ 15 · Глава 1','Получение и применение кислот','SO₃ + H₂O → H₂SO₄','Как получают кислоты и где их используют — от аккумуляторов до пищевой промышленности.',['получение','применение'])
|
||||||
|
+makeCard('theory','Получение кислот','§15','<p>• <b>Кислотный оксид + вода</b> → кислота: SO₃ + H₂O → H₂SO₄; CO₂ + H₂O → H₂CO₃.<br>• <b>Соль + сильная кислота</b> → новая кислота: BaCl₂ + H₂SO₄ → BaSO₄↓ + 2HCl.</p><p><b>Применение:</b> H₂SO₄ — «хлеб химии» (аккумуляторы, удобрения), HCl — в металлургии, H₃PO₄ — пищевая.</p>')
|
||||||
|
+rememberBox(['Бескислородные кислоты не получить «оксид + вода» — у них нет кислорода.','H₂SO₄ — одно из важнейших веществ промышленности.'])
|
||||||
|
+qList(['Из какого оксида получают угольную кислоту?','Где применяют серную кислоту?'])
|
||||||
|
+secNav('p14','p16')+readButton('p15'); wireReadBtn('p15'); }
|
||||||
|
|
||||||
|
function bp16(){ document.getElementById('p16-body').innerHTML =
|
||||||
|
hero(7,'§ 16 · Глава 1','Основания','Me(OH)ₙ','Гидроксиды металлов: растворимые щёлочи и нерастворимые основания.',['щёлочи','индикатор'])
|
||||||
|
+makeCard('theory','Основания','§16','<p><b>Основание</b> — сложное вещество из атома металла и гидроксогрупп OH. По растворимости: <b>щёлочи</b> (растворимые: NaOH, KOH, Ca(OH)₂, Ba(OH)₂) и <b>нерастворимые</b> (Cu(OH)₂, Fe(OH)₃, Mg(OH)₂).</p><p>Щёлочи изменяют окраску индикаторов: лакмус → синий, фенолфталеин → малиновый, метилоранж → жёлтый.</p>')
|
||||||
|
+flag('Классификатор оснований','Раздели основания на щёлочи и нерастворимые.','<div id="c-base-cls"></div>')
|
||||||
|
+wgt('Индикатор: фенолфталеин в щёлочи','<div id="c-base-ind"></div>')
|
||||||
|
+rememberBox(['Щёлочи — растворимые основания активных металлов (I–II групп).','Фенолфталеин — главный индикатор на щёлочь (малиновый).'])
|
||||||
|
+qList(['Назови три щёлочи.','Почему Cu(OH)₂ не относят к щелочам?'])
|
||||||
|
+secNav('p15','p17')+readButton('p16'); wireReadBtn('p16'); }
|
||||||
|
|
||||||
|
function bp17(){ document.getElementById('p17-body').innerHTML =
|
||||||
|
hero(8,'§ 17 · Глава 1','Химические свойства оснований','нейтрализация','Реакции щелочей и нерастворимых оснований: соль, вода и разложение при нагревании.',['+ кислота','+ оксид','разложение'])
|
||||||
|
+makeCard('theory','Свойства оснований','§17','<p>• <b>Нейтрализация:</b> основание + кислота → соль + вода: NaOH + HCl → NaCl + H₂O.<br>• Щёлочь + <b>кислотный оксид</b> → соль + вода: 2NaOH + CO₂ → Na₂CO₃ + H₂O.<br>• <b>Разложение</b> нерастворимых оснований при нагревании: Cu(OH)₂ →(t) CuO + H₂O.</p>')
|
||||||
|
+wgt('Индикатор: нейтрализация (двигай pH)','<div id="c-neutral-ind"></div>')
|
||||||
|
+rememberBox(['Реакция нейтрализации идёт всегда (образуется вода).','Щёлочи термически устойчивы, нерастворимые основания — разлагаются.'])
|
||||||
|
+qList(['Допиши: KOH + H₂SO₄ → ?','Что образуется при нагревании Fe(OH)₃?'])
|
||||||
|
+secNav('p16','p18')+readButton('p17'); wireReadBtn('p17'); }
|
||||||
|
|
||||||
|
function bp18(){ document.getElementById('p18-body').innerHTML =
|
||||||
|
hero(9,'§ 18 · Глава 1','Получение оснований · Лаб. 1 · ПР 2','CuSO₄ + 2NaOH → Cu(OH)₂↓','Как получают щёлочи и нерастворимые основания; лабораторный опыт и реакция нейтрализации.',['Лаб.1','ПР.2','осадок'])
|
||||||
|
+makeCard('theory','Получение оснований','§18','<p>• <b>Щёлочи:</b> активный металл + вода (2Na + 2H₂O → 2NaOH + H₂↑) или основный оксид + вода.<br>• <b>Нерастворимые основания:</b> щёлочь + соль → основание↓ + соль: CuSO₄ + 2NaOH → Cu(OH)₂↓ + Na₂SO₄.</p>')
|
||||||
|
+makeCard('lab','Лабораторный опыт 1 · Получение нерастворимого основания',null,'<p>К раствору соли меди(II) добавь раствор щёлочи — выпадает <b>голубой осадок</b> Cu(OH)₂.</p><div class="tt-row"><div><div style="text-align:center;color:#0891b2">'+tube({color:'#a5f3fc',precipitate:'#22d3ee',label:'Cu(OH)₂'})+'</div><div class="tt-cap">Голубой осадок Cu(OH)₂↓</div></div></div><p>CuSO₄ + 2NaOH → Cu(OH)₂↓ + Na₂SO₄</p>')
|
||||||
|
+makeCard('lab','Практическая работа 2 · Реакция нейтрализации',null,'<p>К щёлочи с фенолфталеином (малиновый) приливай кислоту — окраска исчезает в точке нейтрализации.</p>'+wgt('Двигай pH: исчезновение малиновой окраски','<div id="c-pr2-ind"></div>'))
|
||||||
|
+rememberBox(['Cu(OH)₂ — голубой, Fe(OH)₃ — бурый, Fe(OH)₂ — зеленоватый.','Нерастворимое основание получают только из щёлочи + соль.'])
|
||||||
|
+qList(['Как получить Fe(OH)₃ из соли железа(III)?','Что наблюдают в точке нейтрализации с фенолфталеином?'])
|
||||||
|
+secNav('p17','p19')+readButton('p18'); wireReadBtn('p18'); }
|
||||||
|
|
||||||
|
function bp19(){ document.getElementById('p19-body').innerHTML =
|
||||||
|
hero(1,'§ 19 · Глава 1','Соли. Состав и классификация','катион + анион','Соли — продукт встречи кислоты и основания; растворимость определяют по таблице.',['растворимость','классификация'])
|
||||||
|
+makeCard('theory','Соли','§19','<p><b>Соль</b> — сложное вещество из катионов металла (или NH₄⁺) и анионов кислотного остатка: NaCl, CaCO₃, CuSO₄. Это продукт замещения атомов H в кислоте на металл.</p><p>По составу: <b>средние</b> (NaCl, Na₂SO₄), <b>кислые</b> (NaHCO₃), <b>основные</b> (CuOHCl). Растворимость определяют по <b>таблице растворимости</b>.</p>')
|
||||||
|
+flag('Таблица растворимости','Кликни катион и анион — узнаешь, растворима ли соль и выпадет ли осадок.','<div id="c-salt-sol"></div>')
|
||||||
|
+rememberBox(['Все нитраты и соли натрия/калия растворимы.','Нерастворимая соль выпадает в осадок ↓.'])
|
||||||
|
+qList(['Растворим ли BaSO₄? А CaCO₃?','Из какой кислоты и основания получают Na₂SO₄?'])
|
||||||
|
+secNav('p18','p20')+readButton('p19'); wireReadBtn('p19'); }
|
||||||
|
|
||||||
|
function bp20(){ document.getElementById('p20-body').innerHTML =
|
||||||
|
hero(2,'§ 20 · Глава 1','Химические свойства солей · Лаб. 2','↓ ↑ H₂O','Реакции ионного обмена и вытеснение металлов из растворов солей.',['РИО','соль + Me','Лаб.2'])
|
||||||
|
+makeCard('theory','Свойства солей','§20','<p>• <b>Соль + соль</b> → две новые соли (если ↓): AgNO₃ + NaCl → AgCl↓ + NaNO₃.<br>• <b>Соль + щёлочь</b> → новое основание↓ + соль.<br>• <b>Соль + кислота</b> → новая соль + кислота (если ↓↑).<br>• <b>Соль + металл</b>: более активный металл вытесняет менее активный: Fe + CuSO₄ → FeSO₄ + Cu.</p>')
|
||||||
|
+flag('Таблица растворимости (предскажи осадок)','Проверь, образуется ли осадок в реакции обмена.','<div id="c-salt-sol2"></div>')
|
||||||
|
+flag('Ряд активности (соль + металл)','Кликни металл — увидишь, какие металлы он вытесняет из растворов солей.','<div id="c-salt-act"></div>')
|
||||||
|
+makeCard('lab','Лабораторный опыт 2 · Соль + металл',null,'<p>Опусти железный гвоздь в раствор CuSO₄ — он покрывается <b>красным налётом меди</b>, раствор бледнеет. Fe + CuSO₄ → FeSO₄ + Cu.</p>')
|
||||||
|
+rememberBox(['РИО идёт, если образуется осадок, газ или вода.','Металл вытесняет из соли только менее активный металл.'])
|
||||||
|
+qList(['Вытеснит ли серебро медь из CuSO₄?','Допиши: BaCl₂ + Na₂SO₄ → ?'])
|
||||||
|
+secNav('p19','p21')+readButton('p20'); wireReadBtn('p20'); }
|
||||||
|
|
||||||
|
function bp21(){ document.getElementById('p21-body').innerHTML =
|
||||||
|
hero(3,'§ 21 · Глава 1','Получение и применение солей','много способов','Соли получают десятком способов — это перекрёсток всех классов.',['способы','применение'])
|
||||||
|
+makeCard('theory','Способы получения солей','§21','<p>• кислота + основание (нейтрализация)<br>• кислота + основный оксид<br>• кислота + металл<br>• кислота + соль<br>• щёлочь + кислотный оксид<br>• соль + соль<br>• соль + металл<br>• металл + неметалл (2Na + Cl₂ → 2NaCl)</p><p><b>Применение:</b> NaCl — пища, Na₂CO₃ — стекло и мыло, CaCO₃ — строительство, KNO₃ — удобрения.</p>')
|
||||||
|
+rememberBox(['Соль — продукт реакции кислоты и основания (или их «родственников»).','Один и тот же продукт можно получить разными способами.'])
|
||||||
|
+qList(['Назови три способа получить CuSO₄.','Где применяют поваренную соль NaCl?'])
|
||||||
|
+secNav('p20','p22')+readButton('p21'); wireReadBtn('p21'); }
|
||||||
|
|
||||||
|
function bp22(){ document.getElementById('p22-body').innerHTML =
|
||||||
|
hero(4,'§ 22 · Глава 1','Взаимосвязь классов · ПР 3','генетическая связь','Все классы связаны цепочками превращений — от простого вещества до соли.',['генетика','ПР.3'])
|
||||||
|
+makeCard('theory','Генетическая связь','§22','<p><b>Ряд металла:</b> металл → основный оксид → основание → соль<br>Na → Na₂O → NaOH → NaCl</p><p><b>Ряд неметалла:</b> неметалл → кислотный оксид → кислота → соль<br>S → SO₃ → H₂SO₄ → Na₂SO₄</p><p>Эти ряды «встречаются» в солях — продукте реакции кислоты и основания.</p>')
|
||||||
|
+flag('Генетическая карта классов','Кликни по стрелке-переходу — увидишь реакцию-пример. Два ряда (металл и неметалл) сходятся в соли.','<div id="c-genetic"></div>')
|
||||||
|
+'<div class="insight-box"><div class="insight-title">Цепочка превращений</div><p>Ca → CaO → Ca(OH)₂ → CaCl₂. Попробуй записать уравнение каждого перехода!</p></div>'
|
||||||
|
+makeCard('lab','Практическая работа 3 · Экспериментальные задачи',null,'<p>По выданным реактивам осуществи цепочку превращений и докажи получение каждого вещества (например, получи из меди — оксид меди, затем соль).</p>')
|
||||||
|
+rememberBox(['Металл и неметалл — начала двух генетических рядов.','Соль — точка встречи кислотного и основного «миров».'])
|
||||||
|
+qList(['Запиши цепочку S → SO₂ → … → Na₂SO₃.','Какой класс стоит между оксидом металла и солью?'])
|
||||||
|
+secNav('p21','p23')+readButton('p22'); wireReadBtn('p22'); }
|
||||||
|
|
||||||
|
function bp23(){ document.getElementById('p23-body').innerHTML =
|
||||||
|
hero(5,'§ 23 · Глава 1','Решение расчётных задач','по уравнениям','Расчёты массы и объёма по уравнениям реакций классов неорганических веществ.',['расчёты','мольные отношения'])
|
||||||
|
+makeCard('rule','Алгоритм','§23','<ol><li>Записать и уравнять уравнение.</li><li>Найти n известного: n=m/M (или V/Vm).</li><li>По коэффициентам — n искомого.</li><li>Перейти к m=n·M (или V=n·Vm).</li></ol>')
|
||||||
|
+flag('Пошаговый решатель по уравнению','Выбери задачу и раскрывай решение по шагам.','<div class="fld"><label>Задача</label><select id="c-calc-pick"></select></div><div class="out" id="c-calc-out"></div><div class="fld"><button class="btn" id="c-calc-step">Следующий шаг ▸</button><button class="btn" id="c-calc-all">Показать всё</button></div>')
|
||||||
|
+rememberBox(['Мольное отношение берут из коэффициентов.','Для газов используют Vm=22,4 л/моль.'])
|
||||||
|
+secNav('p22','final1')+readButton('p23'); wireReadBtn('p23'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 1','Босс: классы неорганических соединений','оксиды · кислоты · основания · соли','Шесть интегрированных задач на всю главу. Победи босса — получи ачивку «Классы веществ покорены».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>Оксиды</h3><div class="main-f">осн · кисл · амф</div></div><div class="fcard"><h3>Кислоты</h3><div class="main-f">HₓAc, основность</div></div><div class="fcard"><h3>Основания</h3><div class="main-f">Me(OH)ₙ</div></div><div class="fcard highlight"><h3>Соли</h3><div class="main-f">катион + анион</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи ниже — за каждую +5 XP, за полную победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p23',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 2 · «Периодический закон и периодическая система»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 2 — indigo */
|
||||||
|
:root{ --pri:#4f46e5; --pri-d:#4338ca; --pri-l:#818cf8; --pri-soft:#e0e7ff; --sec-acc:#4f46e5; --sec-acc-d:#4338ca; --sec-acc-soft:#e0e7ff; }
|
||||||
|
html.dark{ --bg:#12122b; --card:#1b1b3a; --card-soft:#20204a; --text:#e0e7ff; --muted:#a5a5d8; --border:#2e2e5c; --pri-soft:rgba(79,70,229,.22); --sec-acc-soft:rgba(79,70,229,.22); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#3730a3 0%,#4f46e5 55%,#818cf8 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 2'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch2_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 2</h1>
|
||||||
|
<div class="hdr-sub">Систематизация элементов, амфотерность, естественные семейства, периодический закон Д. И. Менделеева</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Главный закон химии</h2>
|
||||||
|
<p>В 1869 году Д. И. Менделеев расположил элементы в порядке возрастания атомной массы — и увидел, что их свойства повторяются периодически. Так родилась периодическая система, по которой можно предсказывать свойства веществ.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p24')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 24</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p24" class="sec"><div class="sec-header"><span class="sec-num">§ 24</span><h2 class="sec-h">Систематизация химических элементов</h2></div><div id="p24-body"></div></section>
|
||||||
|
<section id="sec-p25" class="sec"><div class="sec-header"><span class="sec-num">§ 25</span><h2 class="sec-h">Понятие об амфотерности · Лаб. 3</h2></div><div id="p25-body"></div></section>
|
||||||
|
<section id="sec-p26" class="sec"><div class="sec-header"><span class="sec-num">§ 26</span><h2 class="sec-h">Естественные семейства элементов</h2></div><div id="p26-body"></div></section>
|
||||||
|
<section id="sec-p27" class="sec"><div class="sec-header"><span class="sec-num">§ 27</span><h2 class="sec-h">Периодический закон Д. И. Менделеева</h2></div><div id="p27-body"></div></section>
|
||||||
|
<section id="sec-p28" class="sec"><div class="sec-header"><span class="sec-num">§ 28</span><h2 class="sec-h">Периодическая система химических элементов</h2></div><div id="p28-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 2 · «Периодический закон и периодическая система» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch2', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch2_progress', achKey:'chemistry8_ch2_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p24',num:'§ 24',name:'Систематизация элементов',sub:'Me / неMe'},
|
||||||
|
{id:'p25',num:'§ 25',name:'Амфотерность · Лаб.3',sub:'Zn(OH)₂'},
|
||||||
|
{id:'p26',num:'§ 26',name:'Естественные семейства',sub:'щелочные, галогены'},
|
||||||
|
{id:'p27',num:'§ 27',name:'Периодический закон',sub:'Менделеев, 1869'},
|
||||||
|
{id:'p28',num:'§ 28',name:'Периодическая система',sub:'период · группа'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 2!', final1_tasks:'Периодический закон освоен!' };
|
||||||
|
['p24','p25','p26','p27','p28'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p24:{title:'§24 Систематизация',rows:[['Металлы','отдают электроны'],['Неметаллы','принимают электроны'],['Амфотерные','двойственны']]},
|
||||||
|
p25:{title:'§25 Амфотерность',rows:[['Амфотерный','+ кислота И + щёлочь'],['Примеры','Zn(OH)₂, Al(OH)₃'],['Лаб.3','получение Zn(OH)₂']]},
|
||||||
|
p26:{title:'§26 Семейства',rows:[['Щелочные','Li, Na, K (гр. I)'],['Щёлочноземельные','Ca, Sr, Ba (гр. II)'],['Галогены','F, Cl, Br, I (гр. VII)'],['Инертные','He, Ne, Ar (гр. VIII)']]},
|
||||||
|
p27:{title:'§27 ПЗ',rows:[['Менделеев','1869 г.'],['Закон','свойства периодичны'],['Основа','атомная масса → заряд ядра']]},
|
||||||
|
p28:{title:'§28 ПС',rows:[['Период','строка (1–7)'],['Группа','столбец (I–VIII)'],['№ группы','высшая валентность']]},
|
||||||
|
final1:{title:'Финал главы 2',rows:[['§§24–28','периодический закон'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p24',html:'Металлы (слева и снизу в ПСХЭ) отдают электроны, неметаллы (справа и сверху) — принимают. Между ними — амфотерные элементы.'},
|
||||||
|
{sec:'p25',html:'Амфотерный гидроксид (Zn(OH)₂, Al(OH)₃) реагирует и с кислотой (как основание), и со щёлочью (как кислота).'},
|
||||||
|
{sec:'p26',html:'Элементы одной группы образуют естественное семейство со сходными свойствами: щелочные металлы, галогены, инертные газы.'},
|
||||||
|
{sec:'p27',html:'Периодический закон: свойства элементов периодически зависят от заряда ядра их атомов.'},
|
||||||
|
{sec:'p28',html:'Номер периода = число электронных слоёв, номер группы = число внешних электронов (высшая валентность).'},
|
||||||
|
{sec:'final1',html:'Семейства, амфотерность, период и группа — повтори перед боссом.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p24:[
|
||||||
|
{q:'Где в ПСХЭ расположены металлы?',opts:['Справа сверху','Слева и снизу','Только в группе VIII','В центре'],a:1,ex:'Металлы — слева и снизу от диагонали B–At.'},
|
||||||
|
{q:'Неметаллы при реакциях обычно…',opts:['Отдают электроны','Принимают электроны','Не реагируют','Образуют щёлочи'],a:1,ex:'Неметаллы принимают электроны (окислители).'},
|
||||||
|
{q:'Какой элемент проявляет двойственные (амфотерные) свойства?',opts:['Na','Cl','Zn','He'],a:2,ex:'Цинк — амфотерный элемент.'}
|
||||||
|
],
|
||||||
|
p25:[
|
||||||
|
{q:'Амфотерный гидроксид реагирует…',opts:['Только с кислотой','Только со щёлочью','И с кислотой, и со щёлочью','Ни с чем'],a:2,ex:'В этом и состоит амфотерность.'},
|
||||||
|
{q:'Zn(OH)₂ + 2HCl → … (Zn(OH)₂ ведёт себя как…)',opts:['Кислота','Основание','Соль','Оксид'],a:1,ex:'С кислотой — как основание: ZnCl₂ + H₂O.'},
|
||||||
|
{q:'Какого цвета осадок Zn(OH)₂?',opts:['Голубой','Белый','Бурый','Чёрный'],a:1,ex:'Zn(OH)₂ — белый студенистый осадок.'}
|
||||||
|
],
|
||||||
|
p26:[
|
||||||
|
{q:'Щелочные металлы — это элементы группы…',opts:['I','II','VII','VIII'],a:0,ex:'Li, Na, K — главная подгруппа I группы.'},
|
||||||
|
{q:'Галогены — это…',opts:['Li, Na, K','Be, Mg, Ca','F, Cl, Br, I','He, Ne, Ar'],a:2,ex:'Галогены — VII группа.'},
|
||||||
|
{q:'Инертные (благородные) газы почти не реагируют, потому что…',opts:['Они тяжёлые','Их внешний слой завершён','Они металлы','У них нет ядра'],a:1,ex:'Завершённый внешний электронный слой — устойчивость.'}
|
||||||
|
],
|
||||||
|
p27:[
|
||||||
|
{q:'В каком году открыт периодический закон?',opts:['1689','1769','1869','1969'],a:2,ex:'Д. И. Менделеев, 1869 год.'},
|
||||||
|
{q:'В основу первой системы Менделеев положил…',opts:['Заряд ядра','Относительную атомную массу','Число нейтронов','Радиус атома'],a:1,ex:'Тогда расположил по возрастанию атомной массы.'},
|
||||||
|
{q:'Современная формулировка: свойства зависят от…',opts:['Массы атома','Заряда ядра атома','Цвета вещества','Температуры'],a:1,ex:'Свойства периодичны по заряду ядра.'}
|
||||||
|
],
|
||||||
|
p28:[
|
||||||
|
{q:'Номер периода показывает число…',opts:['Протонов','Электронных слоёв','Нейтронов','Групп'],a:1,ex:'Номер периода = число электронных слоёв.'},
|
||||||
|
{q:'Номер группы (главной подгруппы) показывает число…',opts:['Нейтронов','Внешних электронов','Слоёв','Изотопов'],a:1,ex:'Число внешних электронов = высшая валентность.'},
|
||||||
|
{q:'Сколько всего периодов в ПСХЭ?',hint:'строки',unit:'',a:7,ex:'7 периодов.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'Цинк по свойствам — это…',opts:['Типичный металл','Типичный неметалл','Амфотерный элемент','Инертный газ'],a:2,ex:'Zn амфотерен.'},
|
||||||
|
{q:'К какому семейству относится хлор Cl?',opts:['Щелочные','Галогены','Инертные','ЩЗМ'],a:1,ex:'Cl — галоген (VII группа).'},
|
||||||
|
{q:'Год открытия периодического закона:',hint:'',unit:'',a:1869,ex:'1869.'},
|
||||||
|
{q:'Номер группы натрия Na (число внешних электронов):',hint:'I группа',unit:'',a:1,ex:'1 внешний электрон.'},
|
||||||
|
{q:'Сколько электронных слоёв у элемента 3-го периода?',hint:'= номер периода',unit:'',a:3,ex:'3 слоя.'},
|
||||||
|
{q:'Сколько внешних электронов у инертных газов (VIII группа)?',hint:'завершённый слой',unit:'',a:8,ex:'8 (у He — 2).'},
|
||||||
|
{q:'К какому семейству относится калий K?',opts:['Галогены','Щелочные металлы','Инертные','ЩЗМ'],a:1,ex:'K — щелочной металл (I группа).'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function wgt(title, inner){ return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg> '+title+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
|
||||||
|
window.BUILDERS = { p24:bp24, p25:bp25, p26:bp26, p27:bp27, p28:bp28, final1:bfinal };
|
||||||
|
|
||||||
|
function bp24(){ document.getElementById('p24-body').innerHTML =
|
||||||
|
hero(3,'§ 24 · Глава 2','Систематизация химических элементов','Me · неMe','Первые попытки навести порядок среди элементов: разделение на металлы и неметаллы.',['металлы','неметаллы','амфотерные'])
|
||||||
|
+makeCard('theory','Металлы и неметаллы','§24','<p>Элементы делят на <b>металлы</b> (отдают электроны, образуют основные оксиды и основания) и <b>неметаллы</b> (принимают электроны, образуют кислотные оксиды и кислоты). Граница условна — у диагонали B–Si–As–Te стоят <b>амфотерные</b> элементы (Be, Al, Zn).</p>')
|
||||||
|
+flag('Интерактивная ПСХЭ: металлы и неметаллы','Нажимай кнопки-режимы и кликай по элементам — увидишь, как распределены металлы, неметаллы и металлоиды.','<div id="c-pt-metals"></div>')
|
||||||
|
+rememberBox(['Металлы — слева и снизу, неметаллы — справа и сверху.','Амфотерные элементы — у диагонали.'])
|
||||||
|
+qList(['Назови три металла и три неметалла.','Почему граница между ними условна?'])
|
||||||
|
+secNav(null,'p25')+readButton('p24'); wireReadBtn('p24'); }
|
||||||
|
|
||||||
|
function bp25(){ document.getElementById('p25-body').innerHTML =
|
||||||
|
hero(9,'§ 25 · Глава 2','Понятие об амфотерности · Лаб. 3','Zn(OH)₂','Некоторые гидроксиды ведут себя двойственно — реагируют и с кислотами, и со щелочами.',['амфотерность','Zn(OH)₂','Лаб.3'])
|
||||||
|
+makeCard('theory','Амфотерность','§25','<p><b>Амфотерные</b> гидроксиды (Zn(OH)₂, Al(OH)₃, Be(OH)₂) проявляют свойства и оснований, и кислот:</p><p>как <b>основание</b>: Zn(OH)₂ + 2HCl → ZnCl₂ + 2H₂O<br>как <b>кислота</b>: Zn(OH)₂ + 2NaOH → Na₂[Zn(OH)₄]</p>')
|
||||||
|
+flag('Опыт: амфотерность Zn(OH)₂','Добавь к осадку кислоту или щёлочь — в обоих случаях он растворяется.','<div id="c-amph"></div>')
|
||||||
|
+makeCard('lab','Лабораторный опыт 3 · Получение гидроксида цинка',null,'<p>К раствору соли цинка прилей немного щёлочи — выпадет <b>белый осадок</b> Zn(OH)₂. Раздели на две пробирки: в одну добавь кислоту, в другую — избыток щёлочи. Осадок растворяется в обоих случаях — это и есть амфотерность.</p><p>ZnSO₄ + 2NaOH → Zn(OH)₂↓ + Na₂SO₄</p>')
|
||||||
|
+rememberBox(['Амфотерный = и основание, и кислота одновременно.','Zn(OH)₂ и Al(OH)₃ — главные примеры в 8 классе.'])
|
||||||
|
+qList(['Допиши: Al(OH)₃ + NaOH → ?','Как доказать амфотерность гидроксида цинка?'])
|
||||||
|
+secNav('p24','p26')+readButton('p25'); wireReadBtn('p25'); }
|
||||||
|
|
||||||
|
function bp26(){ document.getElementById('p26-body').innerHTML =
|
||||||
|
hero(4,'§ 26 · Глава 2','Естественные семейства элементов','группы','Элементы со схожими свойствами объединяются в семейства — это подсказало структуру системы.',['щелочные','галогены','инертные'])
|
||||||
|
+makeCard('theory','Семейства элементов','§26','<p>Элементы со сходными свойствами образуют <b>естественные семейства</b>:</p><ul><li><b>Щелочные металлы</b> (Li, Na, K, Rb, Cs) — очень активные, бурно реагируют с водой.</li><li><b>Щёлочноземельные</b> (Ca, Sr, Ba) — активные металлы.</li><li><b>Галогены</b> (F, Cl, Br, I) — активные неметаллы.</li><li><b>Инертные газы</b> (He, Ne, Ar) — почти не реагируют.</li></ul>')
|
||||||
|
+flag('ПСХЭ: подсветка семейств','Нажми кнопку семейства — соответствующие элементы подсветятся в системе.','<div id="c-pt-fam"></div>')
|
||||||
|
+rememberBox(['Элементы одной группы — одно семейство.','Активность щелочных металлов растёт вниз по группе, галогенов — убывает.'])
|
||||||
|
+qList(['Назови все галогены.','Почему инертные газы малоактивны?'])
|
||||||
|
+secNav('p25','p27')+readButton('p26'); wireReadBtn('p26'); }
|
||||||
|
|
||||||
|
function bp27(){ document.getElementById('p27-body').innerHTML =
|
||||||
|
hero(5,'§ 27 · Глава 2','Периодический закон Д. И. Менделеева','1869','Открытие, изменившее химию: свойства элементов повторяются периодически.',['Менделеев','периодичность'])
|
||||||
|
+makeCard('theory','Периодический закон','§27','<p>В 1869 году <b>Д. И. Менделеев</b> расположил элементы по возрастанию относительной атомной массы и заметил: свойства повторяются через определённые промежутки — <b>периодически</b>.</p><div class="def-box">Современная формулировка: <b>свойства химических элементов и их соединений находятся в периодической зависимости от заряда ядра атомов</b>.</div><p>Менделеев даже предсказал свойства ещё не открытых элементов (галлий, германий) — и оказался прав!</p>')
|
||||||
|
+'<div class="insight-box"><div class="insight-title">Почему «периодический»</div><p>Двигаясь по периоду слева направо, металлические свойства ослабевают, неметаллические — усиливаются. В начале следующего периода всё повторяется снова.</p></div>'
|
||||||
|
+rememberBox(['Основа закона — заряд ядра (порядковый номер), а не масса.','Менделеев предсказал неоткрытые элементы по «пустым клеткам».'])
|
||||||
|
+qList(['Сформулируй периодический закон.','Что предсказал Менделеев благодаря своей системе?'])
|
||||||
|
+secNav('p26','p28')+readButton('p27'); wireReadBtn('p27'); }
|
||||||
|
|
||||||
|
function bp28(){ document.getElementById('p28-body').innerHTML =
|
||||||
|
hero(6,'§ 28 · Глава 2','Периодическая система химических элементов','период · группа','Графическое выражение закона: таблица из периодов и групп, по которой «читают» элемент.',['период','группа','подгруппа'])
|
||||||
|
+makeCard('theory','Структура системы','§28','<p><b>Период</b> — горизонтальная строка (всего 7). Номер периода = число электронных слоёв в атоме.<br><b>Группа</b> — вертикальный столбец (I–VIII). Номер группы = число внешних электронов = высшая валентность.<br>Группы делятся на <b>главную</b> (А) и <b>побочную</b> (Б) подгруппы.</p>')
|
||||||
|
+flag('ПСХЭ: периоды и группы','Подсвети период или группу — увидишь, как устроена система.','<div id="c-pt-struct"></div>')
|
||||||
|
+rememberBox(['Номер периода = число электронных слоёв.','Номер группы = число внешних электронов.','7 периодов, 8 групп (главные подгруппы).'])
|
||||||
|
+qList(['Сколько внешних электронов у элементов II группы?','В каком периоде элемент с тремя электронными слоями?'])
|
||||||
|
+secNav('p27','final1')+readButton('p28'); wireReadBtn('p28'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 2','Босс: периодический закон','семейства · период · группа','Пять интегрированных задач по всей главе. Победи босса — ачивка «Периодический закон освоен».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>Период</h3><div class="main-f">= число слоёв</div></div><div class="fcard"><h3>Группа</h3><div class="main-f">= внешние e⁻</div></div><div class="fcard"><h3>Семейства</h3><div class="main-f">щелочные · галогены</div></div><div class="fcard highlight"><h3>Закон</h3><div class="main-f">периодичность по Z</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p28',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 3 · «Строение атома»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 3 — blue */
|
||||||
|
:root{ --pri:#2563eb; --pri-d:#1d4ed8; --pri-l:#60a5fa; --pri-soft:#dbeafe; --sec-acc:#2563eb; --sec-acc-d:#1d4ed8; --sec-acc-soft:#dbeafe; }
|
||||||
|
html.dark{ --bg:#0a1428; --card:#102137; --card-soft:#13294a; --text:#dbeafe; --muted:#93c5fd; --border:#1e3a5f; --pri-soft:rgba(37,99,235,.22); --sec-acc-soft:rgba(37,99,235,.22); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#1e3a8a 0%,#2563eb 55%,#60a5fa 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 3'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch3_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 3</h1>
|
||||||
|
<div class="hdr-sub">Строение атома, нуклиды и изотопы, электронные облака и орбитали, электронные оболочки, периодичность</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Что внутри атома</h2>
|
||||||
|
<p>Атом неделим химически, но состоит из ядра (протоны и нейтроны) и движущихся вокруг электронов. Именно строение электронных оболочек объясняет, почему элементы ведут себя так, а не иначе — и почему работает периодический закон.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p29')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 29</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p29" class="sec"><div class="sec-header"><span class="sec-num">§ 29</span><h2 class="sec-h">Строение атома. Атомный номер</h2></div><div id="p29-body"></div></section>
|
||||||
|
<section id="sec-p30" class="sec"><div class="sec-header"><span class="sec-num">§ 30</span><h2 class="sec-h">Массовое число атома. Нуклиды</h2></div><div id="p30-body"></div></section>
|
||||||
|
<section id="sec-p31" class="sec"><div class="sec-header"><span class="sec-num">§ 31</span><h2 class="sec-h">Изотопы. Явление радиоактивности</h2></div><div id="p31-body"></div></section>
|
||||||
|
<section id="sec-p32" class="sec"><div class="sec-header"><span class="sec-num">§ 32</span><h2 class="sec-h">Состояние электронов. Электронное облако. Орбиталь</h2></div><div id="p32-body"></div></section>
|
||||||
|
<section id="sec-p33" class="sec"><div class="sec-header"><span class="sec-num">§ 33</span><h2 class="sec-h">Строение электронных оболочек атомов</h2></div><div id="p33-body"></div></section>
|
||||||
|
<section id="sec-p34" class="sec"><div class="sec-header"><span class="sec-num">§ 34</span><h2 class="sec-h">Периодичность изменения свойств атомов</h2></div><div id="p34-body"></div></section>
|
||||||
|
<section id="sec-p35" class="sec"><div class="sec-header"><span class="sec-num">§ 35</span><h2 class="sec-h">Характеристика элемента по положению в ПС</h2></div><div id="p35-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 3 · «Строение атома» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch3', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch3_progress', achKey:'chemistry8_ch3_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p29',num:'§ 29',name:'Строение атома',sub:'ядро + e⁻'},
|
||||||
|
{id:'p30',num:'§ 30',name:'Массовое число. Нуклиды',sub:'A = Z + N'},
|
||||||
|
{id:'p31',num:'§ 31',name:'Изотопы. Радиоактивность',sub:'тот же Z, разный N'},
|
||||||
|
{id:'p32',num:'§ 32',name:'Электронное облако. Орбиталь',sub:'s, p, d'},
|
||||||
|
{id:'p33',num:'§ 33',name:'Электронные оболочки',sub:'слои, 2n²'},
|
||||||
|
{id:'p34',num:'§ 34',name:'Периодичность свойств',sub:'тренды'},
|
||||||
|
{id:'p35',num:'§ 35',name:'Характеристика элемента',sub:'паспорт'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 3!', final1_tasks:'Строение атома освоено!' };
|
||||||
|
['p29','p30','p31','p32','p33','p34','p35'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p29:{title:'§29 Атом',rows:[['Ядро','протоны p⁺ + нейтроны n⁰'],['Электроны','e⁻ вокруг ядра'],['Z','= число протоков = заряд ядра'],['Атом','нейтрален: p⁺ = e⁻']]},
|
||||||
|
p30:{title:'§30 Нуклиды',rows:[['A','массовое число'],['A = Z + N',''],['Нуклид','атом с данными Z и N']]},
|
||||||
|
p31:{title:'§31 Изотопы',rows:[['Изотопы','один Z, разный N'],['Пример','³⁵Cl и ³⁷Cl'],['A_r','среднее по изотопам']]},
|
||||||
|
p32:{title:'§32 Орбиталь',rows:[['Облако','область вероятного нахождения e⁻'],['s','сфера'],['p','гантель']]},
|
||||||
|
p33:{title:'§33 Оболочки',rows:[['Слой','уровень энергии'],['Ёмкость','2n² электронов'],['Внешние e⁻','определяют свойства']]},
|
||||||
|
p34:{title:'§34 Периодичность',rows:[['По периоду →','радиус ↓, неметалл ↑'],['По группе ↓','радиус ↑, металл ↑']]},
|
||||||
|
p35:{title:'§35 Характеристика',rows:[['Положение','период, группа'],['Строение','слои электронов'],['Свойства','Me/неMe, валентность']]},
|
||||||
|
final1:{title:'Финал главы 3',rows:[['§§29–35','строение атома'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p29',html:'Атом состоит из ядра (протоны + нейтроны) и электронов. Число протонов = заряду ядра = порядковому номеру Z.'},
|
||||||
|
{sec:'p30',html:'Массовое число A = число протонов + число нейтронов = Z + N.'},
|
||||||
|
{sec:'p31',html:'Изотопы — атомы одного элемента с разным числом нейтронов (одинаковый Z, разный A).'},
|
||||||
|
{sec:'p32',html:'Электронное облако (орбиталь) — область, где электрон бывает чаще всего. s-облако — сфера, p-облако — гантель.'},
|
||||||
|
{sec:'p33',html:'Электроны располагаются по слоям; ёмкость слоя — 2n² электронов. Внешний слой определяет химические свойства.'},
|
||||||
|
{sec:'p34',html:'По периоду слева направо радиус атома уменьшается; вниз по группе — растёт.'},
|
||||||
|
{sec:'p35',html:'Зная положение элемента, можно описать строение его атома и предсказать свойства.'},
|
||||||
|
{sec:'final1',html:'Z, A=Z+N, изотопы, слои электронов, периодичность — повтори перед боссом.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p29:[
|
||||||
|
{q:'Из чего состоит ядро атома?',opts:['Из электронов','Из протонов и нейтронов','Только из протонов','Из молекул'],a:1,ex:'Ядро = протоны p⁺ + нейтроны n⁰.'},
|
||||||
|
{q:'Чему равно число протонов в атоме?',opts:['Массовому числу','Порядковому номеру Z','Числу нейтронов','Номеру периода'],a:1,ex:'Число протонов = Z (заряд ядра).'},
|
||||||
|
{q:'Сколько электронов в нейтральном атоме натрия (Z=11)?',hint:'= числу протонов',unit:'',a:11,ex:'Атом нейтрален: e⁻ = p⁺ = 11.'}
|
||||||
|
],
|
||||||
|
p30:[
|
||||||
|
{q:'Массовое число A равно…',opts:['Z − N','Z + N','N − Z','Z · N'],a:1,ex:'A = Z + N.'},
|
||||||
|
{q:'В атоме углерода Z=6, A=12. Сколько нейтронов?',hint:'N = A − Z',unit:'',a:6,ex:'12 − 6 = 6.'},
|
||||||
|
{q:'В атоме ²³Na (Z=11) число нейтронов равно…',hint:'23 − 11',unit:'',a:12,ex:'N = 23 − 11 = 12.'}
|
||||||
|
],
|
||||||
|
p31:[
|
||||||
|
{q:'Изотопы — это атомы одного элемента с разным числом…',opts:['Протонов','Электронов','Нейтронов','Слоёв'],a:2,ex:'Разное число нейтронов → разное A.'},
|
||||||
|
{q:'У изотопов одинаковое число…',opts:['Нейтронов','Протонов (Z)','Массовое число','—'],a:1,ex:'Одинаковый Z (один элемент).'},
|
||||||
|
{q:'Хлор: ³⁵Cl (75%) и ³⁷Cl (25%). Средняя A_r ≈',hint:'(35·75+37·25)/100',unit:'',a:35.5,tol:0.02,ex:'35,5.'}
|
||||||
|
],
|
||||||
|
p32:[
|
||||||
|
{q:'Электронное облако (орбиталь) — это…',opts:['Точная орбита электрона','Область наиболее вероятного нахождения электрона','Ядро атома','Слой нейтронов'],a:1,ex:'Орбиталь — область вероятного нахождения e⁻.'},
|
||||||
|
{q:'Какую форму имеет s-орбиталь?',opts:['Гантель','Сфера','Кольцо','Куб'],a:1,ex:'s-облако — сферическое.'},
|
||||||
|
{q:'Какую форму имеет p-орбиталь?',opts:['Сфера','Гантель','Спираль','Плоскость'],a:1,ex:'p-облако — гантель (объёмная восьмёрка).'}
|
||||||
|
],
|
||||||
|
p33:[
|
||||||
|
{q:'Сколько электронов максимально на первом слое (n=1)?',hint:'2n², n=1',unit:'',a:2,ex:'2·1² = 2.'},
|
||||||
|
{q:'Сколько электронов максимально на втором слое (n=2)?',hint:'2n², n=2',unit:'',a:8,ex:'2·2² = 8.'},
|
||||||
|
{q:'Распределение электронов в атоме натрия (Z=11):',opts:['2,9','2,8,1','8,3','2,8,8'],a:1,ex:'Na: 2 ) 8 ) 1.'},
|
||||||
|
{q:'Что определяет химические свойства атома?',opts:['Число нейтронов','Внешние электроны','Масса ядра','Цвет'],a:1,ex:'Электроны внешнего слоя.'}
|
||||||
|
],
|
||||||
|
p34:[
|
||||||
|
{q:'По периоду слева направо радиус атома…',opts:['Растёт','Уменьшается','Не меняется','Удваивается'],a:1,ex:'Заряд ядра растёт, притягивает сильнее → радиус ↓.'},
|
||||||
|
{q:'Вниз по группе металлические свойства…',opts:['Усиливаются','Ослабевают','Исчезают','Не меняются'],a:0,ex:'Радиус растёт, e⁻ легче отдаётся → металл ↑.'},
|
||||||
|
{q:'Самый активный неметалл (правый верх ПСХЭ):',opts:['Натрий','Фтор','Железо','Гелий'],a:1,ex:'Фтор — самый активный неметалл.'}
|
||||||
|
],
|
||||||
|
p35:[
|
||||||
|
{q:'Что можно определить по положению элемента в ПСХЭ?',opts:['Только цвет','Строение атома и свойства','Только массу','Ничего'],a:1,ex:'Период, группа → строение и свойства.'},
|
||||||
|
{q:'Элемент 3-го периода, II группы — это…',opts:['Na','Mg','Al','Ca'],a:1,ex:'Mg: период 3, группа II.'},
|
||||||
|
{q:'Сколько внешних электронов у элемента VII группы?',hint:'= номер группы',unit:'',a:7,ex:'7 внешних электронов.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'Сколько нейтронов в ³⁹K (Z=19)?',hint:'39 − 19',unit:'',a:20,ex:'N = 20.'},
|
||||||
|
{q:'Изотопы отличаются числом…',opts:['Протонов','Электронов','Нейтронов','Групп'],a:2,ex:'Числом нейтронов.'},
|
||||||
|
{q:'Максимум электронов на 3-м слое (2n², n=3):',hint:'2·9',unit:'',a:18,ex:'18.'},
|
||||||
|
{q:'Распределение e⁻ в атоме кислорода (Z=8):',opts:['2,6','2,8','6,2','8'],a:0,ex:'O: 2 ) 6.'},
|
||||||
|
{q:'По периоду слева направо неметаллические свойства…',opts:['Ослабевают','Усиливаются','Не меняются','Исчезают'],a:1,ex:'Усиливаются.'},
|
||||||
|
{q:'Число внешних электронов у элемента I группы:',hint:'',unit:'',a:1,ex:'1.'},
|
||||||
|
{q:'Сколько протонов в атоме серы (Z=16)?',unit:'',a:16,ex:'Число протонов = Z = 16.'},
|
||||||
|
{q:'Распределение электронов у магния (Z=12):',opts:['2, 8, 2','2, 10','8, 4','2, 8, 8'],a:0,ex:'Mg: 2 ) 8 ) 2.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function wgt(title, inner){ return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="9" fill="none"/></svg> '+title+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
|
||||||
|
window.BUILDERS = { p29:bp29, p30:bp30, p31:bp31, p32:bp32, p33:bp33, p34:bp34, p35:bp35, final1:bfinal };
|
||||||
|
|
||||||
|
function bp29(){ document.getElementById('p29-body').innerHTML =
|
||||||
|
hero(4,'§ 29 · Глава 3','Строение атома. Атомный номер','Z = p⁺ = e⁻','Атом неделим химически, но имеет сложное строение: крошечное ядро и облако электронов.',['ядро','электроны','Z'])
|
||||||
|
+makeCard('theory','Состав атома','§29','<p>Атом состоит из положительно заряженного <b>ядра</b> (протоны p⁺ и нейтроны n⁰) и движущихся вокруг него <b>электронов</b> e⁻. Почти вся масса сосредоточена в ядре.</p><div class="def-box">Число протонов в ядре = <b>заряд ядра</b> = <b>порядковый номер Z</b>. Атом электронейтрален: число электронов равно числу протонов.</div>')
|
||||||
|
+flag('Модель атома: двигай Z','Меняй порядковый номер — собирается ядро и электронные слои элемента.','<div id="c-atom"></div>')
|
||||||
|
+rememberBox(['Z = число протонов = число электронов в нейтральном атоме.','Масса атома сосредоточена в ядре.'])
|
||||||
|
+qList(['Сколько электронов в атоме серы (Z=16)?','Чем определяется заряд ядра?'])
|
||||||
|
+secNav(null,'p30')+readButton('p29'); wireReadBtn('p29'); }
|
||||||
|
|
||||||
|
function bp30(){ document.getElementById('p30-body').innerHTML =
|
||||||
|
hero(5,'§ 30 · Глава 3','Массовое число атома. Нуклиды','A = Z + N','Как считают массу ядра и что такое нуклид — конкретный сорт атома.',['A','нуклид','нейтроны'])
|
||||||
|
+makeCard('theory','Массовое число','§30','<div class="def-box"><b>Массовое число</b> A = число протонов + число нейтронов = Z + N. Отсюда число нейтронов N = A − Z.</div><p><b>Нуклид</b> — вид атомов с определённым числом протонов и нейтронов (например, ¹²C, ²³Na).</p>')
|
||||||
|
+wgt('Калькулятор нуклида: A = Z + N','<div id="c-nuclide"></div>')
|
||||||
|
+rememberBox(['A — целое число (масса в а.е.м. ≈ A).','N = A − Z; протонов всегда Z.'])
|
||||||
|
+qList(['Сколько нейтронов в ²⁷Al (Z=13)?','Чем нуклид отличается от элемента?'])
|
||||||
|
+secNav('p29','p31')+readButton('p30'); wireReadBtn('p30'); }
|
||||||
|
|
||||||
|
function bp31(){ document.getElementById('p31-body').innerHTML =
|
||||||
|
hero(6,'§ 31 · Глава 3','Изотопы. Явление радиоактивности','³⁵Cl, ³⁷Cl','Почему атомная масса хлора — 35,5, и откуда берётся радиоактивность.',['изотопы','A_r','распад'])
|
||||||
|
+makeCard('theory','Изотопы','§31','<p><b>Изотопы</b> — атомы одного элемента с <b>одинаковым числом протонов</b> (Z), но <b>разным числом нейтронов</b> (разное A). Относительная атомная масса элемента — среднее по природной смеси изотопов.</p><p>Ядра некоторых изотопов самопроизвольно распадаются, испуская излучение, — это <b>радиоактивность</b>.</p>')
|
||||||
|
+wgt('Средняя A_r по изотопам','<div id="c-iso"></div>')
|
||||||
|
+rememberBox(['Изотопы: один Z, разный N (и A).','A_r хлора = 35,5 — среднее по ³⁵Cl и ³⁷Cl.'])
|
||||||
|
+qList(['Чем похожи и чем отличаются изотопы?','Почему A_r многих элементов — дробные?'])
|
||||||
|
+secNav('p30','p32')+readButton('p31'); wireReadBtn('p31'); }
|
||||||
|
|
||||||
|
function bp32(){ document.getElementById('p32-body').innerHTML =
|
||||||
|
hero(7,'§ 32 · Глава 3','Состояние электронов. Электронное облако. Орбиталь','s, p, d','Электрон — не шарик на орбите, а «облако вероятности». Его форму описывает орбиталь.',['облако','s','p'])
|
||||||
|
+makeCard('theory','Электронное облако','§32','<p>Электрон в атоме движется не по чёткой орбите, а образует <b>электронное облако</b> — область, где он бывает чаще всего. <b>Орбиталь</b> — это форма такого облака.</p><p><b>s-орбиталь</b> имеет форму сферы, <b>p-орбиталь</b> — форму гантели (объёмной восьмёрки). Есть и более сложные d-орбитали.</p>')
|
||||||
|
+'<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg> Формы электронных облаков</div><div class="orb-row"><div class="orb-item"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="34" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="2"/><circle cx="50" cy="50" r="3" fill="var(--pri)"/></svg><div class="orb-lab">s — сфера</div></div><div class="orb-item"><svg viewBox="0 0 100 100"><ellipse cx="50" cy="30" rx="16" ry="24" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="2"/><ellipse cx="50" cy="70" rx="16" ry="24" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="2"/><circle cx="50" cy="50" r="3" fill="var(--pri)"/></svg><div class="orb-lab">p — гантель</div></div></div></div>'
|
||||||
|
+rememberBox(['Орбиталь — форма электронного облака, а не орбита.','s — сфера, p — гантель.'])
|
||||||
|
+qList(['Какую форму имеет s-облако? А p-облако?','Чем облако отличается от орбиты планеты?'])
|
||||||
|
+secNav('p31','p33')+readButton('p32'); wireReadBtn('p32'); }
|
||||||
|
|
||||||
|
function bp33(){ document.getElementById('p33-body').innerHTML =
|
||||||
|
hero(8,'§ 33 · Глава 3','Строение электронных оболочек атомов','2n²','Как электроны распределяются по слоям и почему именно внешний слой решает всё.',['слои','2n²','конфигурация'])
|
||||||
|
+makeCard('theory','Электронные слои','§33','<p>Электроны располагаются вокруг ядра по <b>слоям</b> (энергетическим уровням). Максимальная ёмкость слоя — <b>2n²</b> электронов: 1-й слой — 2, 2-й — 8, 3-й — 18.</p><p>Слои заполняются от ближнего к ядру. <b>Внешний слой</b> (валентные электроны) определяет химические свойства элемента. Например, натрий: 2 ) 8 ) 1 — один внешний электрон, поэтому активный металл.</p>')
|
||||||
|
+flag('Конструктор электронных оболочек','Двигай Z — увидишь распределение электронов по слоям (2 ) 8 ) 1 …).','<div id="c-shells"></div>')
|
||||||
|
+rememberBox(['Ёмкость слоя — 2n² электронов.','Свойства определяет внешний (последний) слой.'])
|
||||||
|
+qList(['Запиши распределение электронов для алюминия (Z=13).','Сколько внешних электронов у кислорода?'])
|
||||||
|
+secNav('p32','p34')+readButton('p33'); wireReadBtn('p33'); }
|
||||||
|
|
||||||
|
function bp34(){ document.getElementById('p34-body').innerHTML =
|
||||||
|
hero(9,'§ 34 · Глава 3','Периодичность изменения свойств атомов','тренды','Свойства атомов меняются закономерно — по периодам и группам.',['радиус','металличность'])
|
||||||
|
+makeCard('theory','Закономерности','§34','<p><b>По периоду</b> (слева направо): заряд ядра растёт, радиус атома уменьшается, металлические свойства ослабевают, неметаллические — усиливаются.<br><b>По группе</b> (сверху вниз): число слоёв растёт, радиус увеличивается, металлические свойства усиливаются.</p>')
|
||||||
|
+flag('ПСХЭ: тренды свойств','Подсвети период или группу — увидишь направление изменения свойств.','<div id="c-trend"></div>')
|
||||||
|
+rememberBox(['По периоду → неметалл усиливается; по группе ↓ металл усиливается.','Самый активный неметалл — фтор (правый верх).'])
|
||||||
|
+qList(['Что активнее: Na или K? Почему?','Как меняется радиус по периоду?'])
|
||||||
|
+secNav('p33','p35')+readButton('p34'); wireReadBtn('p34'); }
|
||||||
|
|
||||||
|
function bp35(){ document.getElementById('p35-body').innerHTML =
|
||||||
|
hero(1,'§ 35 · Глава 3','Характеристика элемента по положению','паспорт элемента','Зная клетку элемента в системе, можно «прочитать» строение его атома и свойства.',['период','группа','паспорт'])
|
||||||
|
+makeCard('rule','Алгоритм характеристики','§35','<ol><li>Найти Z, период и группу.</li><li>Определить состав атома: протоны = Z, электроны = Z.</li><li>Распределить электроны по слоям (число слоёв = период, внешних e⁻ = группа).</li><li>Сделать вывод: металл/неметалл, валентность, типичные соединения.</li></ol>')
|
||||||
|
+flag('Генератор «паспорта элемента»','Кликни любой элемент — получишь его полную характеристику по положению в ПСХЭ.','<div id="c-passport"></div>')
|
||||||
|
+rememberBox(['Период = число слоёв, группа = число внешних электронов.','По положению можно предсказать свойства элемента.'])
|
||||||
|
+qList(['Дай характеристику атома магния по плану.','Сколько слоёв и внешних электронов у хлора?'])
|
||||||
|
+secNav('p34','final1')+readButton('p35'); wireReadBtn('p35'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 3','Босс: строение атома','Z · A=Z+N · слои · периодичность','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Строение атома освоено».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>Состав</h3><div class="main-f">ядро + e⁻</div></div><div class="fcard"><h3>Масса</h3><div class="main-f">A = Z + N</div></div><div class="fcard"><h3>Слои</h3><div class="main-f">2n² электронов</div></div><div class="fcard highlight"><h3>Тренд</h3><div class="main-f">период → неметалл↑</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p35',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 4 · «Химическая связь»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 4 — green */
|
||||||
|
:root{ --pri:#059669; --pri-d:#047857; --pri-l:#34d399; --pri-soft:#d1fae5; --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
|
||||||
|
html.dark{ --bg:#0a1a12; --card:#10271c; --card-soft:#143524; --text:#d1fae5; --muted:#6ee7b7; --border:#1d4634; --pri-soft:rgba(5,150,105,.22); --sec-acc-soft:rgba(5,150,105,.22); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#064e3b 0%,#059669 55%,#34d399 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 4'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_mol.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch4_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 4</h1>
|
||||||
|
<div class="hdr-sub">Природа связи, ковалентная (полярная и неполярная), ионная и металлическая связь, кристаллические решётки</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Почему атомы держатся вместе</h2>
|
||||||
|
<p>Атомы соединяются, чтобы завершить внешний электронный слой и стать устойчивее. В зависимости от того, как именно они «делят» электроны, возникают разные типы химической связи — а от них зависят свойства веществ.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p36')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 36</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p36" class="sec"><div class="sec-header"><span class="sec-num">§ 36</span><h2 class="sec-h">Природа химической связи</h2></div><div id="p36-body"></div></section>
|
||||||
|
<section id="sec-p37" class="sec"><div class="sec-header"><span class="sec-num">§ 37</span><h2 class="sec-h">Ковалентная связь</h2></div><div id="p37-body"></div></section>
|
||||||
|
<section id="sec-p38" class="sec"><div class="sec-header"><span class="sec-num">§ 38</span><h2 class="sec-h">Полярная и неполярная связь. Электроотрицательность · Лаб. 4</h2></div><div id="p38-body"></div></section>
|
||||||
|
<section id="sec-p39" class="sec"><div class="sec-header"><span class="sec-num">§ 39</span><h2 class="sec-h">Ионная связь</h2></div><div id="p39-body"></div></section>
|
||||||
|
<section id="sec-p40" class="sec"><div class="sec-header"><span class="sec-num">§ 40</span><h2 class="sec-h">Металлическая связь. Межмолекулярное взаимодействие</h2></div><div id="p40-body"></div></section>
|
||||||
|
<section id="sec-p41" class="sec"><div class="sec-header"><span class="sec-num">§ 41</span><h2 class="sec-h">Кристаллическое состояние вещества</h2></div><div id="p41-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 4 · «Химическая связь» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch4', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch4_progress', achKey:'chemistry8_ch4_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p36',num:'§ 36',name:'Природа связи',sub:'октет · энергия'},
|
||||||
|
{id:'p37',num:'§ 37',name:'Ковалентная связь',sub:'общие e-пары'},
|
||||||
|
{id:'p38',num:'§ 38',name:'Полярность · ЭО · Лаб.4',sub:'ΔЭО → тип'},
|
||||||
|
{id:'p39',num:'§ 39',name:'Ионная связь',sub:'передача e⁻'},
|
||||||
|
{id:'p40',num:'§ 40',name:'Металлическая связь',sub:'электронный газ'},
|
||||||
|
{id:'p41',num:'§ 41',name:'Кристаллические решётки',sub:'4 типа'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 4!', final1_tasks:'Химическая связь освоена!' };
|
||||||
|
['p36','p37','p38','p39','p40','p41'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p36:{title:'§36 Природа',rows:[['Причина','завершить внешний слой'],['Октет','8 e⁻ — устойчиво'],['Энергия','при связи выделяется']]},
|
||||||
|
p37:{title:'§37 Ковалентная',rows:[['Связь','общие электронные пары'],['Между','неметаллами'],['Кратность','одинарная/двойная/тройная']]},
|
||||||
|
p38:{title:'§38 Полярность',rows:[['Неполярная','ΔЭО ≈ 0 (H₂, Cl₂)'],['Полярная','ΔЭО ≠ 0 (HCl, H₂O)'],['ЭО','тяга к электронам']]},
|
||||||
|
p39:{title:'§39 Ионная',rows:[['Связь','передача e⁻'],['Между','Me и неMe'],['Ионы','катион + анион']]},
|
||||||
|
p40:{title:'§40 Металлическая',rows:[['Связь','ион-остовы + e-газ'],['Свойства','ковкость, блеск, ток'],['Межмолек.','слабое притяжение']]},
|
||||||
|
p41:{title:'§41 Решётки',rows:[['Ионная','соли — тугоплавкие'],['Атомная','алмаз — твёрдый'],['Молекулярная','лёд — летучий'],['Металлическая','металлы']]},
|
||||||
|
final1:{title:'Финал главы 4',rows:[['§§36–41','химическая связь'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p36',html:'Атомы соединяются, чтобы завершить внешний электронный слой (правило октета) и понизить энергию — стать устойчивее.'},
|
||||||
|
{sec:'p37',html:'Ковалентная связь — общие электронные пары между атомами (обычно неметаллов).'},
|
||||||
|
{sec:'p38',html:'Если ΔЭО ≈ 0 — связь неполярная; если ЭО разная — полярная (пара смещена к более ЭО атому).'},
|
||||||
|
{sec:'p39',html:'Ионная связь — полная передача электронов от металла к неметаллу; образуются ионы.'},
|
||||||
|
{sec:'p40',html:'Металлическая связь — положительные ион-остовы в «электронном газе» из общих электронов.'},
|
||||||
|
{sec:'p41',html:'Тип кристаллической решётки определяет свойства: ионная — тугоплавкая, молекулярная — летучая.'},
|
||||||
|
{sec:'final1',html:'Ковалентная, ионная, металлическая связь и решётки — повтори перед боссом.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p36:[
|
||||||
|
{q:'Почему атомы образуют химические связи?',opts:['Случайно','Чтобы завершить внешний слой и стать устойчивее','Чтобы стать тяжелее','Из-за нагревания'],a:1,ex:'Стремление к завершённому внешнему слою (октету).'},
|
||||||
|
{q:'Завершённый внешний слой содержит (для большинства) … электронов',opts:['2','4','8','18'],a:2,ex:'Октет — 8 электронов (у He — 2).'},
|
||||||
|
{q:'При образовании связи энергия…',opts:['Поглощается','Выделяется','Не меняется','Исчезает'],a:1,ex:'Связанное состояние устойчивее → энергия выделяется.'}
|
||||||
|
],
|
||||||
|
p37:[
|
||||||
|
{q:'Ковалентная связь образуется за счёт…',opts:['Передачи электронов','Общих электронных пар','Притяжения ионов','Электронного газа'],a:1,ex:'Общие электронные пары.'},
|
||||||
|
{q:'Между какими атомами обычно ковалентная связь?',opts:['Металл + металл','Металл + неметалл','Неметалл + неметалл','Ион + ион'],a:2,ex:'Между атомами неметаллов.'},
|
||||||
|
{q:'Сколько общих пар в молекуле N₂ (тройная связь)?',hint:'тройная',unit:'',a:3,ex:'Тройная связь — 3 общие пары.'}
|
||||||
|
],
|
||||||
|
p38:[
|
||||||
|
{q:'Связь в молекуле H₂ (одинаковые атомы) — это…',opts:['Полярная','Неполярная','Ионная','Металлическая'],a:1,ex:'ΔЭО = 0 → неполярная.'},
|
||||||
|
{q:'Связь в молекуле HCl — это…',opts:['Неполярная','Полярная ковалентная','Ионная','Металлическая'],a:1,ex:'ΔЭО ≈ 0,9 → полярная ковалентная.'},
|
||||||
|
{q:'Электроотрицательность — это…',opts:['Масса атома','Способность притягивать общие электроны','Заряд ядра','Число нейтронов'],a:1,ex:'ЭО — мера притяжения общих электронов.'}
|
||||||
|
],
|
||||||
|
p39:[
|
||||||
|
{q:'Ионная связь образуется за счёт…',opts:['Общих пар','Полной передачи электронов','Электронного газа','Нейтронов'],a:1,ex:'Электрон полностью переходит к неметаллу.'},
|
||||||
|
{q:'Связь в NaCl — это…',opts:['Ковалентная','Ионная','Металлическая','Водородная'],a:1,ex:'Na (металл) + Cl (неметалл), ΔЭО велика → ионная.'},
|
||||||
|
{q:'Натрий в NaCl превращается в…',opts:['Na⁻','Na⁺','Na₂','атом Na'],a:1,ex:'Отдаёт электрон → катион Na⁺.'}
|
||||||
|
],
|
||||||
|
p40:[
|
||||||
|
{q:'Металлическую связь образует…',opts:['Передача e⁻','Общие пары','Ион-остовы и свободные электроны','Притяжение молекул'],a:2,ex:'«Электронный газ» связывает ион-остовы.'},
|
||||||
|
{q:'Чем объясняется электропроводность металлов?',opts:['Ионами','Свободными электронами','Нейтронами','Молекулами'],a:1,ex:'Свободные электроны переносят заряд.'},
|
||||||
|
{q:'Металлическая связь характерна для…',opts:['Солей','Газов','Металлов и сплавов','Кислот'],a:2,ex:'Металлы и сплавы.'}
|
||||||
|
],
|
||||||
|
p41:[
|
||||||
|
{q:'Вещества с ионной решёткой (соли) обычно…',opts:['Летучие','Тугоплавкие, твёрдые','Газы','Жидкие'],a:1,ex:'Ионная решётка прочная → высокие t плавления.'},
|
||||||
|
{q:'Какую решётку имеет алмаз?',opts:['Ионную','Атомную','Молекулярную','Металлическую'],a:1,ex:'Атомная решётка → очень твёрдый.'},
|
||||||
|
{q:'Вещества с молекулярной решёткой (лёд, CO₂) обычно…',opts:['Тугоплавкие','Летучие, легкоплавкие','Проводят ток','Очень твёрдые'],a:1,ex:'Слабые межмолекулярные силы → летучи.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'Связь в Cl₂ — это…',opts:['Полярная','Неполярная ковалентная','Ионная','Металлическая'],a:1,ex:'Одинаковые атомы → неполярная.'},
|
||||||
|
{q:'Связь в H₂O — это…',opts:['Неполярная','Полярная ковалентная','Ионная','Металлическая'],a:1,ex:'O и H разной ЭО → полярная.'},
|
||||||
|
{q:'Связь в KCl — это…',opts:['Ковалентная','Ионная','Металлическая','Водородная'],a:1,ex:'Металл + неметалл → ионная.'},
|
||||||
|
{q:'Сколько электронов в завершённом внешнем слое (октет)?',hint:'',unit:'',a:8,ex:'8.'},
|
||||||
|
{q:'Какой тип решётки у поваренной соли NaCl?',opts:['Молекулярная','Ионная','Атомная','—'],a:1,ex:'Ионная решётка.'},
|
||||||
|
{q:'Что переносит электрический ток в металле?',opts:['Ионы','Свободные электроны','Молекулы','Протоны'],a:1,ex:'Свободные электроны.'},
|
||||||
|
{q:'Какая связь в молекуле N₂?',opts:['Ионная','Ковалентная неполярная','Металлическая','Полярная'],a:1,ex:'Одинаковые атомы → неполярная ковалентная.'},
|
||||||
|
{q:'Сколько общих электронных пар в молекуле N₂ (тройная связь)?',unit:'',a:3,ex:'3 пары.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
|
||||||
|
window.BUILDERS = { p36:bp36, p37:bp37, p38:bp38, p39:bp39, p40:bp40, p41:bp41, final1:bfinal };
|
||||||
|
|
||||||
|
function bp36(){ document.getElementById('p36-body').innerHTML =
|
||||||
|
hero(5,'§ 36 · Глава 4','Природа химической связи','правило октета','Что заставляет атомы соединяться: стремление к устойчивости завершённого слоя.',['октет','энергия'])
|
||||||
|
+makeCard('theory','Почему возникает связь','§36','<p>Атомы инертных газов почти не реагируют — у них <b>завершённый внешний слой</b> (8 электронов, у He — 2). Остальные атомы стремятся достичь такого же устойчивого состояния, отдавая, принимая или обобществляя электроны. Так возникает <b>химическая связь</b>.</p><div class="def-box">При образовании связи система переходит в более устойчивое состояние с меньшей энергией — поэтому энергия <b>выделяется</b>.</div>')
|
||||||
|
+rememberBox(['Цель связи — завершить внешний слой (октет).','Связанное состояние устойчивее, энергия выделяется.'])
|
||||||
|
+qList(['Почему инертные газы малоактивны?','Сколько электронов в завершённом слое?'])
|
||||||
|
+secNav(null,'p37')+readButton('p36'); wireReadBtn('p36'); }
|
||||||
|
|
||||||
|
function bp37(){ document.getElementById('p37-body').innerHTML =
|
||||||
|
hero(6,'§ 37 · Глава 4','Ковалентная связь','общие пары','Атомы неметаллов делятся электронами, образуя общие пары — это ковалентная связь.',['общие пары','неметаллы'])
|
||||||
|
+makeCard('theory','Общие электронные пары','§37','<p><b>Ковалентная связь</b> образуется за счёт <b>общих электронных пар</b> между атомами (обычно неметаллов). Каждый атом «достраивает» свой внешний слой за счёт общих электронов.</p><p>Связь бывает <b>одинарная</b> (H–H, одна пара), <b>двойная</b> (O=O, две пары), <b>тройная</b> (N≡N, три пары).</p>')
|
||||||
|
+flag('Тип связи по электроотрицательности','Выбери два атома — увидишь тип связи и смещение общей пары. Для одинаковых атомов связь неполярная ковалентная.','<div id="c-bond1"></div>')
|
||||||
|
+rememberBox(['Ковалентная = общие электронные пары.','Чем больше общих пар, тем прочнее связь.'])
|
||||||
|
+qList(['Сколько общих пар в молекуле O₂?','Между какими атомами образуется ковалентная связь?'])
|
||||||
|
+secNav('p36','p38')+readButton('p37'); wireReadBtn('p37'); }
|
||||||
|
|
||||||
|
function bp38(){ document.getElementById('p38-body').innerHTML =
|
||||||
|
hero(4,'§ 38 · Глава 4','Полярная и неполярная связь. ЭО · Лаб. 4','ΔЭО','Если атомы тянут электроны одинаково — связь неполярна; если по-разному — полярна.',['ЭО','полярность','Лаб.4'])
|
||||||
|
+makeCard('theory','Электроотрицательность и полярность','§38','<div class="def-box"><b>Электроотрицательность (ЭО)</b> — способность атома притягивать к себе общие электроны.</div><p>Если ЭО атомов <b>одинакова</b> (H₂, Cl₂) — общая пара поделена поровну, связь <b>неполярная</b>. Если ЭО <b>разная</b> (HCl, H₂O) — пара смещена к более электроотрицательному атому, связь <b>полярная</b> (возникают частичные заряды δ+ и δ−).</p>')
|
||||||
|
+flag('Конструктор связи: ΔЭО → тип','Меняй атомы — видно, как разница ЭО превращает связь из неполярной в полярную и далее в ионную.','<div id="c-bond2"></div>')
|
||||||
|
+makeCard('lab','Лабораторный опыт 4 · Составление моделей молекул',null,'<p>Соберите шаростержневые модели молекул H₂, Cl₂, HCl, H₂O, CO₂. Определите для каждой тип связи (полярная/неполярная) и форму молекулы. Сравните: в симметричных молекулах (CO₂) полярные связи «компенсируются», и молекула в целом неполярна.</p>')
|
||||||
|
+flag('3D-модели молекул','Выбери молекулу и вращай её мышью. Снизу — молярная масса, тип связи, форма и полярность.','<div id="c-mol"></div>')
|
||||||
|
+rememberBox(['ΔЭО ≈ 0 → неполярная; ΔЭО заметна → полярная; ΔЭО велика → ионная.','Более электроотрицательный атом получает δ−.'])
|
||||||
|
+qList(['Полярна ли связь в Cl₂? А в HCl?','Что показывает электроотрицательность?'])
|
||||||
|
+secNav('p37','p39')+readButton('p38'); wireReadBtn('p38'); }
|
||||||
|
|
||||||
|
function bp39(){ document.getElementById('p39-body').innerHTML =
|
||||||
|
hero(9,'§ 39 · Глава 4','Ионная связь','Na⁺ Cl⁻','Когда разница ЭО очень велика, электрон переходит полностью — образуются ионы.',['ионы','передача e⁻'])
|
||||||
|
+makeCard('theory','Передача электронов','§39','<p>Если разница электроотрицательностей <b>очень велика</b> (металл + активный неметалл), электрон <b>полностью переходит</b> от металла к неметаллу. Образуются заряженные частицы — <b>ионы</b>: катион (+) и анион (−), которые притягиваются друг к другу. Это <b>ионная связь</b>.</p>')
|
||||||
|
+'<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg> Образование ионной связи Na + Cl</div>'
|
||||||
|
+'<div class="bt-stage"><svg viewBox="0 0 260 80" class="bt-svg"><circle cx="55" cy="40" r="26" fill="var(--pri)" opacity=".15" stroke="var(--pri)" stroke-width="2"/><text x="55" y="46" text-anchor="middle" font-size="15" font-weight="800" fill="currentColor">Na</text><text x="55" y="14" text-anchor="middle" font-size="11" font-weight="800" fill="var(--pri)">−1e⁻</text><path d="M85 40 h40" stroke="var(--pri)" stroke-width="2" marker-end="url(#ar)"/><defs><marker id="ar" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0 0 L6 3 L0 6 z" fill="var(--pri)"/></marker></defs><circle cx="205" cy="40" r="26" fill="var(--fail)" opacity=".15" stroke="var(--fail)" stroke-width="2"/><text x="205" y="46" text-anchor="middle" font-size="15" font-weight="800" fill="currentColor">Cl</text><text x="205" y="14" text-anchor="middle" font-size="11" font-weight="800" fill="var(--fail)">+1e⁻</text></svg></div>'
|
||||||
|
+'<div class="out ok"><span class="bd">Na − e⁻ → Na⁺ | Cl + e⁻ → Cl⁻ → притяжение Na⁺Cl⁻ (ионная связь)</span></div></div>'
|
||||||
|
+rememberBox(['Ионная связь = полная передача электронов.','Металл → катион (+), неметалл → анион (−).'])
|
||||||
|
+qList(['Во что превращается хлор в NaCl?','Почему между Na и Cl именно ионная связь?'])
|
||||||
|
+secNav('p38','p40')+readButton('p39'); wireReadBtn('p39'); }
|
||||||
|
|
||||||
|
function bp40(){ document.getElementById('p40-body').innerHTML =
|
||||||
|
hero(7,'§ 40 · Глава 4','Металлическая связь. Межмолекулярное взаимодействие','электронный газ','В металлах общие электроны принадлежат сразу всем атомам — это объясняет их свойства.',['e-газ','ковкость','ток'])
|
||||||
|
+makeCard('theory','Металлическая связь','§40','<p>В металле внешние электроны легко отрываются и свободно перемещаются между положительными <b>ион-остовами</b>, образуя <b>«электронный газ»</b>. Эти общие электроны связывают все атомы — это <b>металлическая связь</b>.</p><p>Она объясняет свойства металлов: <b>электро- и теплопроводность</b> (электроны переносят заряд и энергию), <b>ковкость</b>, <b>металлический блеск</b>.</p><p><b>Межмолекулярное взаимодействие</b> — слабое притяжение между молекулами (включая водородную связь в воде) — удерживает молекулы в жидкостях и твёрдых телах.</p>')
|
||||||
|
+'<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg> Модель «электронного газа»</div><div class="bt-stage"><svg viewBox="0 0 220 90" class="bt-svg">'
|
||||||
|
+[ '40,30','90,30','140,30','40,65','90,65','140,65','180,48' ].map(function(p){var xy=p.split(',');return '<circle cx="'+xy[0]+'" cy="'+xy[1]+'" r="13" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="1.5"/><text x="'+xy[0]+'" y="'+(+xy[1]+4)+'" text-anchor="middle" font-size="9" font-weight="800" fill="currentColor">+</text>';}).join('')
|
||||||
|
+[ '65,48','115,48','160,32','65,20','115,78','30,48' ].map(function(p){var xy=p.split(',');return '<circle cx="'+xy[0]+'" cy="'+xy[1]+'" r="3" fill="var(--ok)"/>';}).join('')
|
||||||
|
+'</svg></div><div class="out"><span class="bd">«+» — ион-остовы металла, зелёные точки — свободные электроны (электронный газ).</span></div></div>'
|
||||||
|
+rememberBox(['Металлическая связь = ион-остовы + электронный газ.','Свободные электроны → проводимость, блеск, ковкость.'])
|
||||||
|
+qList(['Почему металлы проводят электрический ток?','Что такое «электронный газ»?'])
|
||||||
|
+secNav('p39','p41')+readButton('p40'); wireReadBtn('p40'); }
|
||||||
|
|
||||||
|
function bp41(){ document.getElementById('p41-body').innerHTML =
|
||||||
|
hero(8,'§ 41 · Глава 4','Кристаллическое состояние вещества','4 типа решёток','Тип связи определяет тип кристаллической решётки — а она определяет свойства вещества.',['решётки','свойства'])
|
||||||
|
+makeCard('theory','Типы кристаллических решёток','§41','<p>В кристалле частицы расположены упорядоченно, образуя <b>решётку</b>. От её типа зависят свойства вещества:</p>'
|
||||||
|
+'<div class="lat-grid">'
|
||||||
|
+'<div class="lat-card"><h4>Ионная</h4><div class="lat-ex">NaCl, CaCO₃</div><ul><li>узлы — ионы</li><li>твёрдые, тугоплавкие</li><li>хрупкие</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Атомная</h4><div class="lat-ex">алмаз, SiO₂</div><ul><li>узлы — атомы (ков. связь)</li><li>очень твёрдые</li><li>тугоплавкие</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Молекулярная</h4><div class="lat-ex">лёд, CO₂, I₂</div><ul><li>узлы — молекулы</li><li>летучие, легкоплавкие</li><li>мягкие</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Металлическая</h4><div class="lat-ex">Fe, Cu, Al</div><ul><li>ион-остовы + e-газ</li><li>ковкие, проводят ток</li><li>блеск</li></ul></div>'
|
||||||
|
+'</div>')
|
||||||
|
+flag('3D-модель кристаллической решётки','Выбери тип решётки и вращай её мышью.','<div id="c-lattice"></div>')
|
||||||
|
+rememberBox(['Ионная и атомная решётки — прочные, тугоплавкие.','Молекулярная — летучая, легкоплавкая.','Тип решётки ← тип связи → свойства.'])
|
||||||
|
+qList(['Какой тип решётки у льда? Почему он легкоплавкий?','Чем объясняется твёрдость алмаза?'])
|
||||||
|
+secNav('p40','final1')+readButton('p41'); wireReadBtn('p41'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 4','Босс: химическая связь','ковалентная · ионная · металлическая','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Химическая связь освоена».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>Ковалентная</h3><div class="main-f">общие пары</div></div><div class="fcard"><h3>Ионная</h3><div class="main-f">передача e⁻</div></div><div class="fcard"><h3>Металлическая</h3><div class="main-f">электронный газ</div></div><div class="fcard highlight"><h3>Решётка</h3><div class="main-f">тип → свойства</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p41',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 5 · «Окислительно-восстановительные реакции»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 5 — orange */
|
||||||
|
:root{ --pri:#ea580c; --pri-d:#c2410c; --pri-l:#fb923c; --pri-soft:#ffedd5; --sec-acc:#ea580c; --sec-acc-d:#c2410c; --sec-acc-soft:#ffedd5; }
|
||||||
|
html.dark{ --bg:#1c1208; --card:#2a1c10; --card-soft:#33240f; --text:#ffedd5; --muted:#fdba74; --border:#4a3115; --pri-soft:rgba(234,88,12,.22); --sec-acc-soft:rgba(234,88,12,.22); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#9a3412 0%,#ea580c 55%,#fb923c 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 5'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch5_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 5</h1>
|
||||||
|
<div class="hdr-sub">Степень окисления, процессы окисления и восстановления, ОВР и метод электронного баланса</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Реакции, в которых электроны меняют хозяина</h2>
|
||||||
|
<p>Горение, ржавление, дыхание, работа батарейки — всё это окислительно-восстановительные реакции. В них одни атомы отдают электроны, другие принимают, и степени окисления меняются.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p42')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 42</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p42" class="sec"><div class="sec-header"><span class="sec-num">§ 42</span><h2 class="sec-h">Степень окисления</h2></div><div id="p42-body"></div></section>
|
||||||
|
<section id="sec-p43" class="sec"><div class="sec-header"><span class="sec-num">§ 43</span><h2 class="sec-h">Процессы окисления и восстановления</h2></div><div id="p43-body"></div></section>
|
||||||
|
<section id="sec-p44" class="sec"><div class="sec-header"><span class="sec-num">§ 44</span><h2 class="sec-h">Окислительно-восстановительные реакции</h2></div><div id="p44-body"></div></section>
|
||||||
|
<section id="sec-p45" class="sec"><div class="sec-header"><span class="sec-num">§ 45</span><h2 class="sec-h">Окислительно-восстановительные реакции вокруг нас</h2></div><div id="p45-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 5 · «Окислительно-восстановительные реакции» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch5', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch5_progress', achKey:'chemistry8_ch5_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p42',num:'§ 42',name:'Степень окисления',sub:'+/− на атоме'},
|
||||||
|
{id:'p43',num:'§ 43',name:'Окисление и восстановление',sub:'отдал/принял e⁻'},
|
||||||
|
{id:'p44',num:'§ 44',name:'ОВР',sub:'электронный баланс'},
|
||||||
|
{id:'p45',num:'§ 45',name:'ОВР вокруг нас',sub:'горение, коррозия'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 5!', final1_tasks:'ОВР освоены!' };
|
||||||
|
['p42','p43','p44','p45'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p42:{title:'§42 С.о.',rows:[['С.о.','условный заряд атома'],['H','+1 (обычно)'],['O','−2 (обычно)'],['Σ','с.о. = 0 (молекула)']]},
|
||||||
|
p43:{title:'§43 Окисл./восст.',rows:[['Окисление','отдача e⁻ (с.о. ↑)'],['Восстановление','приём e⁻ (с.о. ↓)'],['Восстановитель','отдаёт e⁻'],['Окислитель','принимает e⁻']]},
|
||||||
|
p44:{title:'§44 ОВР',rows:[['ОВР','меняются с.о.'],['Метод','электронный баланс'],['Баланс','отдано e⁻ = принято e⁻']]},
|
||||||
|
p45:{title:'§45 Вокруг нас',rows:[['Горение','+ O₂'],['Коррозия','ржавление Fe'],['Батарейка','ток из ОВР'],['Дыхание','окисление глюкозы']]},
|
||||||
|
final1:{title:'Финал главы 5',rows:[['§§42–45','ОВР'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p42',html:'Степень окисления — условный заряд атома. H обычно +1, O обычно −2, сумма с.о. в молекуле = 0.'},
|
||||||
|
{sec:'p43',html:'Окисление — отдача электронов (с.о. растёт). Восстановление — приём электронов (с.о. падает).'},
|
||||||
|
{sec:'p44',html:'В ОВР число отданных электронов = числу принятых (метод электронного баланса).'},
|
||||||
|
{sec:'p45',html:'Горение, коррозия, дыхание, работа батарейки — всё это ОВР.'},
|
||||||
|
{sec:'final1',html:'С.о., окислитель/восстановитель, электронный баланс — повтори перед боссом.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p42:[
|
||||||
|
{q:'Степень окисления — это…',opts:['Масса атома','Условный заряд атома в соединении','Число нейтронов','Валентность ядра'],a:1,ex:'Условный заряд, который был бы у атома, если все связи ионные.'},
|
||||||
|
{q:'Степень окисления кислорода в большинстве соединений:',opts:['+2','−2','0','+6'],a:1,ex:'O обычно −2.'},
|
||||||
|
{q:'Степень окисления серы в H₂SO₄:',hint:'H +1, O −2, Σ=0',unit:'',a:6,ex:'2·(+1)+S+4·(−2)=0 → S=+6.'},
|
||||||
|
{q:'Степень окисления любого простого вещества (O₂, Fe) равна…',hint:'',unit:'',a:0,ex:'У простых веществ с.о. = 0.'}
|
||||||
|
],
|
||||||
|
p43:[
|
||||||
|
{q:'Окисление — это процесс…',opts:['Приёма электронов','Отдачи электронов','Распада ядра','Растворения'],a:1,ex:'Окисление — отдача e⁻ (с.о. растёт).'},
|
||||||
|
{q:'Восстановитель в реакции…',opts:['Принимает e⁻','Отдаёт e⁻','Не меняется','Распадается'],a:1,ex:'Восстановитель отдаёт электроны (сам окисляется).'},
|
||||||
|
{q:'Окислитель — это частица, которая…',opts:['Отдаёт e⁻','Принимает e⁻','Растворяется','Испаряется'],a:1,ex:'Окислитель принимает электроны (сам восстанавливается).'}
|
||||||
|
],
|
||||||
|
p44:[
|
||||||
|
{q:'В основе ОВР лежит…',opts:['Обмен ионами','Переход электронов','Обмен молекулами','Изменение цвета'],a:1,ex:'Переход электронов между частицами.'},
|
||||||
|
{q:'Метод электронного баланса требует, чтобы…',opts:['Масса росла','Число отданных e⁻ = числу принятых','Объём не менялся','Цвет совпадал'],a:1,ex:'Отдано e⁻ = принято e⁻.'},
|
||||||
|
{q:'В реакции 2Mg + O₂ → 2MgO магний…',opts:['Окислитель','Восстановитель','Не участвует','Катализатор'],a:1,ex:'Mg отдаёт электроны → восстановитель.'}
|
||||||
|
],
|
||||||
|
p45:[
|
||||||
|
{q:'Что из перечисленного НЕ является ОВР?',opts:['Горение угля','Ржавление железа','Растворение соли в воде','Работа батарейки'],a:2,ex:'Растворение соли — без изменения с.о.'},
|
||||||
|
{q:'При ржавлении железа Fe…',opts:['Принимает e⁻','Отдаёт e⁻','Не меняется','Испаряется'],a:1,ex:'Fe окисляется (отдаёт e⁻), с.о. растёт.'},
|
||||||
|
{q:'Источник тока в батарейке — это…',opts:['Реакция обмена','ОВР','Растворение','Плавление'],a:1,ex:'Ток возникает за счёт ОВР.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'С.о. азота в HNO₃:',hint:'H +1, O −2',unit:'',a:5,ex:'+1+N+3·(−2)=0 → N=+5.'},
|
||||||
|
{q:'Окисление — это…',opts:['Приём e⁻','Отдача e⁻','Распад','Растворение'],a:1,ex:'Отдача электронов.'},
|
||||||
|
{q:'В 2Na + Cl₂ → 2NaCl хлор является…',opts:['Восстановителем','Окислителем','Катализатором','Растворителем'],a:1,ex:'Cl₂ принимает e⁻ → окислитель.'},
|
||||||
|
{q:'С.о. простого вещества равна…',hint:'',unit:'',a:0,ex:'0.'},
|
||||||
|
{q:'С.о. марганца в KMnO₄:',hint:'K +1, O −2',unit:'',a:7,ex:'+1+Mn+4·(−2)=0 → Mn=+7.'},
|
||||||
|
{q:'Сколько электронов отдаёт Mg при окислении до Mg⁺²?',hint:'',unit:'',a:2,ex:'2 электрона.'},
|
||||||
|
{q:'Степень окисления хлора в HCl?',hint:'H +1, Σ=0',unit:'',a:-1,tol:0.1,step:'1',ex:'Cl: −1.'},
|
||||||
|
{q:'Окислитель в реакции электроны…',opts:['отдаёт','принимает','не меняет','растворяет'],a:1,ex:'Окислитель принимает электроны.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function wgt(title, inner){ return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
|
||||||
|
window.BUILDERS = { p42:bp42, p43:bp43, p44:bp44, p45:bp45, final1:bfinal };
|
||||||
|
|
||||||
|
function bp42(){ document.getElementById('p42-body').innerHTML =
|
||||||
|
hero(6,'§ 42 · Глава 5','Степень окисления','с.о.','Условный заряд атома, который помогает «считать» электроны в соединениях.',['с.о.','H +1','O −2'])
|
||||||
|
+makeCard('theory','Что такое степень окисления','§42','<div class="def-box"><b>Степень окисления</b> — условный заряд атома в соединении, вычисленный из предположения, что все связи ионные.</div><p>Правила: у простых веществ с.о. = 0; водород обычно <b>+1</b>, кислород обычно <b>−2</b>; сумма степеней окисления в молекуле равна <b>0</b>. По этим правилам находят с.о. остальных элементов.</p>')
|
||||||
|
+flag('Калькулятор степени окисления','Введи формулу — определятся степени окисления всех элементов.','<div id="c-ox"></div>')
|
||||||
|
+rememberBox(['У простого вещества с.о. = 0.','H → +1, O → −2 (как правило).','Сумма с.о. в нейтральной молекуле = 0.'])
|
||||||
|
+qList(['Найди с.о. серы в SO₃.','Чему равна с.о. железа в Fe₂O₃?'])
|
||||||
|
+secNav(null,'p43')+readButton('p42'); wireReadBtn('p42'); }
|
||||||
|
|
||||||
|
function bp43(){ document.getElementById('p43-body').innerHTML =
|
||||||
|
hero(5,'§ 43 · Глава 5','Процессы окисления и восстановления','отдал / принял e⁻','Два процесса, которые всегда идут вместе: один атом отдаёт электроны, другой их принимает.',['окисление','восстановление'])
|
||||||
|
+makeCard('theory','Окисление и восстановление','§43','<p><b>Окисление</b> — отдача электронов, степень окисления <b>повышается</b>.<br><b>Восстановление</b> — приём электронов, степень окисления <b>понижается</b>.</p><div class="def-box"><b>Восстановитель</b> отдаёт электроны (сам окисляется). <b>Окислитель</b> принимает электроны (сам восстанавливается).</div>')
|
||||||
|
+'<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M3 12h18"/></svg> Кто есть кто</div><div class="lat-grid"><div class="lat-card"><h4>Восстановитель</h4><ul><li>отдаёт e⁻</li><li>с.о. растёт</li><li>сам окисляется</li><li>пример: Mg⁰ → Mg⁺²</li></ul></div><div class="lat-card"><h4>Окислитель</h4><ul><li>принимает e⁻</li><li>с.о. падает</li><li>сам восстанавливается</li><li>пример: O₂⁰ → O⁻²</li></ul></div></div></div>'
|
||||||
|
+rememberBox(['Окисление — отдача e⁻ (с.о. ↑); восстановление — приём e⁻ (с.о. ↓).','Восстановитель окисляется, окислитель восстанавливается.'])
|
||||||
|
+qList(['Что происходит со степенью окисления при окислении?','Кто отдаёт электроны — окислитель или восстановитель?'])
|
||||||
|
+secNav('p42','p44')+readButton('p43'); wireReadBtn('p43'); }
|
||||||
|
|
||||||
|
function bp44(){ document.getElementById('p44-body').innerHTML =
|
||||||
|
hero(8,'§ 44 · Глава 5','Окислительно-восстановительные реакции','электронный баланс','Реакции с переходом электронов уравнивают особым методом — электронным балансом.',['ОВР','баланс e⁻'])
|
||||||
|
+makeCard('theory','Метод электронного баланса','§44','<p><b>ОВР</b> — реакции, в которых меняются степени окисления (происходит переход электронов). Их уравнивают <b>методом электронного баланса</b>:</p><ol><li>Определить с.о. и найти, кто отдаёт, а кто принимает электроны.</li><li>Записать полуреакции: сколько e⁻ отдано и принято.</li><li>Подобрать множители так, чтобы <b>отдано e⁻ = принято e⁻</b>.</li><li>Перенести множители в уравнение как коэффициенты.</li></ol>')
|
||||||
|
+flag('Пошаговый электронный баланс','Выбери реакцию и разбери метод по шагам.','<div class="fld"><label>Реакция</label><select id="c-redox-pick"></select></div><div class="out" id="c-redox-out"></div><div class="fld"><button class="btn" id="c-redox-step">Следующий шаг ▸</button><button class="btn" id="c-redox-all">Показать всё</button></div>')
|
||||||
|
+rememberBox(['ОВР = изменение степеней окисления.','Главное правило: отдано e⁻ = принято e⁻.'])
|
||||||
|
+qList(['Кто восстановитель в 2Mg + O₂ → 2MgO?','Сколько электронов принимает O₂ в этой реакции?'])
|
||||||
|
+secNav('p43','p45')+readButton('p44'); wireReadBtn('p44'); }
|
||||||
|
|
||||||
|
function bp45(){ document.getElementById('p45-body').innerHTML =
|
||||||
|
hero(9,'§ 45 · Глава 5','ОВР вокруг нас','горение · коррозия · ток','ОВР — не абстракция: они греют, двигают, питают и, увы, разрушают.',['горение','коррозия','батарейка'])
|
||||||
|
+makeCard('theory','ОВР в жизни','§45','<p>Окислительно-восстановительные реакции окружают нас повсюду:</p>'
|
||||||
|
+'<div class="lat-grid">'
|
||||||
|
+'<div class="lat-card"><h4>Горение</h4><ul><li>топливо + O₂</li><li>выделяется тепло</li><li>C + O₂ → CO₂</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Коррозия</h4><ul><li>ржавление железа</li><li>Fe → Fe₂O₃·nH₂O</li><li>разрушает металл</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Дыхание</h4><ul><li>окисление глюкозы</li><li>даёт энергию клеткам</li></ul></div>'
|
||||||
|
+'<div class="lat-card"><h4>Батарейка</h4><ul><li>ток из ОВР</li><li>энергия реакции → электричество</li></ul></div>'
|
||||||
|
+'</div>')
|
||||||
|
+rememberBox(['Горение, дыхание, коррозия, батарейки — это ОВР.','Растворение соли — НЕ ОВР (с.о. не меняется).'])
|
||||||
|
+qList(['Приведи три примера ОВР из жизни.','Почему растворение сахара — не ОВР?'])
|
||||||
|
+secNav('p44','final1')+readButton('p45'); wireReadBtn('p45'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 5','Босс: ОВР','с.о. · окислитель/восстановитель · баланс','Шесть интегрированных задач по всей главе. Победи босса — ачивка «ОВР освоены».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>С.о.</h3><div class="main-f">H +1, O −2, Σ=0</div></div><div class="fcard"><h3>Окисление</h3><div class="main-f">−e⁻, с.о. ↑</div></div><div class="fcard"><h3>Восстановление</h3><div class="main-f">+e⁻, с.о. ↓</div></div><div class="fcard highlight"><h3>Баланс</h3><div class="main-f">отдано = принято</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p45',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Глава 6 · «Растворы»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<style>
|
||||||
|
/* Глава 6 — cyan */
|
||||||
|
:root{ --pri:#0891b2; --pri-d:#0e7490; --pri-l:#22d3ee; --pri-soft:#cffafe; --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
|
||||||
|
html.dark{ --bg:#08191c; --card:#10282d; --card-soft:#143539; --text:#cffafe; --muted:#67e8f9; --border:#1a4046; --pri-soft:rgba(8,145,178,.22); --sec-acc-soft:rgba(8,145,178,.22); }
|
||||||
|
.hdr{background:linear-gradient(110deg,#164e63 0%,#0891b2 55%,#22d3ee 100%)}
|
||||||
|
.hdr::before{content:'ГЛАВА 6'}
|
||||||
|
</style>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_ch6_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Глава 6</h1>
|
||||||
|
<div class="hdr-sub">Смеси и растворы, растворимость, массовая доля и молярная концентрация, вода в жизни человека</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Самые важные смеси на Земле</h2>
|
||||||
|
<p>Морская вода, кровь, лимонад, воздух — это всё растворы и смеси. Химик умеет описывать их состав количественно: массовой долей и молярной концентрацией.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p46')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 46</button>
|
||||||
|
<div class="hero-progress"><span class="hp-label">Прогресс главы</span><div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div><span id="hero-hp-text" class="hp-text">0%</span></div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel"><div class="psel-title">Параграфы главы</div><div id="psel-grid" class="psel-grid"></div></section>
|
||||||
|
|
||||||
|
<section id="sec-p46" class="sec"><div class="sec-header"><span class="sec-num">§ 46</span><h2 class="sec-h">Смеси веществ</h2></div><div id="p46-body"></div></section>
|
||||||
|
<section id="sec-p47" class="sec"><div class="sec-header"><span class="sec-num">§ 47</span><h2 class="sec-h">Растворение веществ в воде</h2></div><div id="p47-body"></div></section>
|
||||||
|
<section id="sec-p48" class="sec"><div class="sec-header"><span class="sec-num">§ 48</span><h2 class="sec-h">Характеристики растворимости веществ</h2></div><div id="p48-body"></div></section>
|
||||||
|
<section id="sec-p49" class="sec"><div class="sec-header"><span class="sec-num">§ 49</span><h2 class="sec-h">Качественные характеристики состава растворов</h2></div><div id="p49-body"></div></section>
|
||||||
|
<section id="sec-p50" class="sec"><div class="sec-header"><span class="sec-num">§ 50</span><h2 class="sec-h">Массовая доля растворённого вещества</h2></div><div id="p50-body"></div></section>
|
||||||
|
<section id="sec-p51" class="sec"><div class="sec-header"><span class="sec-num">§ 51</span><h2 class="sec-h">Молярная концентрация · ПР 4</h2></div><div id="p51-body"></div></section>
|
||||||
|
<section id="sec-p52" class="sec"><div class="sec-header"><span class="sec-num">§ 52</span><h2 class="sec-h">Вода и растворы в жизни человека</h2></div><div id="p52-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал главы</h2></div><div id="final1-body"></div></section>
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Глава 6 · «Растворы» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-ch6', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp', progKey:'chemistry8_ch6_progress', achKey:'chemistry8_ch6_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p46',num:'§ 46',name:'Смеси веществ',sub:'однород./неоднород.'},
|
||||||
|
{id:'p47',num:'§ 47',name:'Растворение в воде',sub:'гидратация'},
|
||||||
|
{id:'p48',num:'§ 48',name:'Растворимость',sub:'s = f(t)'},
|
||||||
|
{id:'p49',num:'§ 49',name:'Качественные характеристики',sub:'насыщ./ненасыщ.'},
|
||||||
|
{id:'p50',num:'§ 50',name:'Массовая доля',sub:'w = m/m'},
|
||||||
|
{id:'p51',num:'§ 51',name:'Молярная концентрация · ПР4',sub:'c = n/V'},
|
||||||
|
{id:'p52',num:'§ 52',name:'Вода в жизни',sub:'значение'},
|
||||||
|
{id:'final1',num:'★',name:'Финал главы',sub:'босс · ачивка',final:true}
|
||||||
|
];
|
||||||
|
window.ACH_LABELS = { start:'Начало главы 6!', final1_tasks:'Растворы освоены!' };
|
||||||
|
['p46','p47','p48','p49','p50','p51','p52'].forEach(function(id){ window.ACH_LABELS[id+'_done']=id.toUpperCase()+' изучен!'; });
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p46:{title:'§46 Смеси',rows:[['Однородные','раствор, воздух, сплав'],['Неоднородные','песок+вода, гранит'],['Разделение','фильтрование, выпаривание']]},
|
||||||
|
p47:{title:'§47 Растворение',rows:[['Раствор','растворитель + в-во'],['Гидратация','вода окружает частицы'],['Тепловой эффект','+ или − тепло']]},
|
||||||
|
p48:{title:'§48 Растворимость',rows:[['Растворимость','г на 100 г воды'],['Зависит','от t (для большинства)'],['Кривая','s = f(t)']]},
|
||||||
|
p49:{title:'§49 Характеристики',rows:[['Ненасыщенный','можно ещё растворить'],['Насыщенный','предел растворения'],['Разбавл./конц.','мало/много в-ва']]},
|
||||||
|
p50:{title:'§50 Массовая доля',rows:[['w','= m(в-ва)/m(р-ра)'],['В %','×100%'],['m(р-ра)','= m(в-ва)+m(воды)']]},
|
||||||
|
p51:{title:'§51 Концентрация',rows:[['c','= n/V'],['Единица','моль/л'],['ПР4','приготовление раствора']]},
|
||||||
|
p52:{title:'§52 Вода',rows:[['Растворитель','№1 в природе'],['Очистка','фильтрование, кипячение'],['В быту','напитки, лекарства']]},
|
||||||
|
final1:{title:'Финал главы 6',rows:[['§§46–52','растворы'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p46',html:'Однородные смеси (растворы) — состав одинаков во всём объёме (воздух, раствор соли, сплав). Неоднородные — видны части (песок + вода).'},
|
||||||
|
{sec:'p47',html:'При растворении частицы вещества окружаются молекулами воды (гидратация). Может выделяться или поглощаться тепло.'},
|
||||||
|
{sec:'p48',html:'Растворимость — масса вещества, растворяющаяся в 100 г воды. У большинства твёрдых веществ растёт с температурой.'},
|
||||||
|
{sec:'p49',html:'Ненасыщенный раствор может растворить ещё; насыщенный — достиг предела при данной температуре.'},
|
||||||
|
{sec:'p50',html:'Массовая доля w = m(вещества) / m(раствора); m(раствора) = m(вещества) + m(воды).'},
|
||||||
|
{sec:'p51',html:'Молярная концентрация c = n/V (моль/л). Сначала найди n = m/M.'},
|
||||||
|
{sec:'p52',html:'Вода — универсальный растворитель и основа жизни; её очищают фильтрованием, отстаиванием, кипячением.'},
|
||||||
|
{sec:'final1',html:'Смеси, растворимость, w и c — повтори перед боссом.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.POOLS = {
|
||||||
|
p46:[
|
||||||
|
{q:'Однородная смесь (раствор) — это смесь, в которой…',opts:['Видны отдельные части','Состав одинаков во всём объёме','Всегда есть осадок','Только газы'],a:1,ex:'В растворе компоненты не различимы.'},
|
||||||
|
{q:'Какая смесь неоднородная?',opts:['Воздух','Раствор сахара','Песок и вода','Сталь'],a:2,ex:'Песок + вода — видны части.'},
|
||||||
|
{q:'Как разделить смесь песка и воды?',opts:['Выпариванием','Фильтрованием','Перегонкой','Намагничиванием'],a:1,ex:'Фильтрование отделяет нерастворимый песок.'}
|
||||||
|
],
|
||||||
|
p47:[
|
||||||
|
{q:'Раствор состоит из…',opts:['Только растворителя','Растворителя и растворённого вещества','Только осадка','Двух газов'],a:1,ex:'Растворитель + растворённое вещество.'},
|
||||||
|
{q:'Процесс окружения частиц вещества молекулами воды называют…',opts:['Гидратацией','Фильтрованием','Кипением','Кристаллизацией'],a:0,ex:'Гидратация.'},
|
||||||
|
{q:'При растворении некоторых веществ раствор нагревается, потому что…',opts:['Вода кипит','Выделяется тепло','Свет','Газ'],a:1,ex:'Тепловой эффект растворения.'}
|
||||||
|
],
|
||||||
|
p48:[
|
||||||
|
{q:'Растворимость измеряют в…',opts:['Граммах на 100 г воды','Литрах','Молях на грамм','Процентах массы ядра'],a:0,ex:'г вещества на 100 г воды.'},
|
||||||
|
{q:'Как меняется растворимость большинства твёрдых веществ при нагревании?',opts:['Падает','Растёт','Не меняется','Исчезает'],a:1,ex:'Обычно растёт с температурой.'},
|
||||||
|
{q:'У какого вещества растворимость почти не зависит от температуры?',opts:['KNO₃','NaCl','сахар','—'],a:1,ex:'У NaCl растворимость почти постоянна.'}
|
||||||
|
],
|
||||||
|
p49:[
|
||||||
|
{q:'Насыщенный раствор — это раствор, в котором…',opts:['Мало вещества','Вещество больше не растворяется при данной t','Есть осадок всегда','Только вода'],a:1,ex:'Достигнут предел растворения.'},
|
||||||
|
{q:'Ненасыщенный раствор…',opts:['Нельзя разбавить','Может растворить ещё вещества','Всегда с осадком','Кипит'],a:1,ex:'Ещё не достиг предела.'},
|
||||||
|
{q:'«Концентрированный» раствор означает, что вещества в нём…',opts:['Мало','Много','Нет','Только газ'],a:1,ex:'Много растворённого вещества.'}
|
||||||
|
],
|
||||||
|
p50:[
|
||||||
|
{q:'Массовая доля w равна…',opts:['m(в-ва)/m(воды)','m(в-ва)/m(раствора)','m(раствора)/m(в-ва)','n/V'],a:1,ex:'w = m(вещества)/m(раствора).'},
|
||||||
|
{q:'В 80 г воды растворили 20 г соли. Чему равна w (%)?',hint:'20/(20+80)·100',unit:'%',a:20,ex:'20/100·100 = 20 %.'},
|
||||||
|
{q:'Масса раствора при растворении 5 г соли в 95 г воды равна…',hint:'5+95',unit:'г',a:100,ex:'m(р-ра)=100 г.'}
|
||||||
|
],
|
||||||
|
p51:[
|
||||||
|
{q:'Молярная концентрация c равна…',opts:['m/V','n/V','n·V','m·M'],a:1,ex:'c = n/V (моль/л).'},
|
||||||
|
{q:'40 г NaOH (M=40) растворили до объёма 1 л. Чему равна c?',hint:'n=m/M=1; c=n/V',unit:'моль/л',a:1,ex:'n=1 моль, c=1/1=1 моль/л.'},
|
||||||
|
{q:'В 2 л раствора содержится 1 моль вещества. Концентрация равна…',hint:'c=n/V',unit:'моль/л',a:0.5,ex:'1/2 = 0,5 моль/л.'}
|
||||||
|
],
|
||||||
|
p52:[
|
||||||
|
{q:'Почему воду называют универсальным растворителем?',opts:['Она прозрачна','Растворяет очень многие вещества','Она лёгкая','Она кипит'],a:1,ex:'Вода растворяет множество веществ.'},
|
||||||
|
{q:'Какой способ НЕ используют для очистки воды?',opts:['Фильтрование','Отстаивание','Кипячение','Замораживание соли в неё'],a:3,ex:'Очищают фильтрованием, отстаиванием, кипячением.'},
|
||||||
|
{q:'Морская вода — это…',opts:['Чистое вещество','Раствор солей','Простое вещество','Оксид'],a:1,ex:'Раствор солей в воде.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'Какая смесь однородная?',opts:['Песок + вода','Молоко','Раствор соли','Гранит'],a:2,ex:'Раствор соли — однородная.'},
|
||||||
|
{q:'Растворимость большинства твёрдых веществ при нагревании…',opts:['Падает','Растёт','Не меняется','—'],a:1,ex:'Растёт.'},
|
||||||
|
{q:'В 150 г воды растворили 50 г соли. w (%) =',hint:'50/200·100',unit:'%',a:25,ex:'25 %.'},
|
||||||
|
{q:'20 г NaOH (M=40) в 0,5 л раствора. c (моль/л) =',hint:'n=0,5; c=0,5/0,5',unit:'моль/л',a:1,ex:'1 моль/л.'},
|
||||||
|
{q:'Раствор, который больше не растворяет вещество, называют…',opts:['Ненасыщенным','Насыщенным','Разбавленным','Чистым'],a:1,ex:'Насыщенный.'},
|
||||||
|
{q:'Молярную концентрацию измеряют в…',opts:['граммах','моль/л','литрах','процентах'],a:1,ex:'моль/л.'},
|
||||||
|
{q:'В 90 г воды растворили 10 г соли. Чему равна w (%)?',hint:'10/100·100',unit:'%',a:10,ex:'10 %.'},
|
||||||
|
{q:'0,5 моль вещества в 0,5 л раствора. Чему равна c (моль/л)?',hint:'c=n/V',unit:'моль/л',a:1,ex:'0,5/0,5=1 моль/л.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rememberBox(items){ return '<div class="remember-box"><div class="remember-box-title"><svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>'; }
|
||||||
|
function qList(items){ return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>'; }
|
||||||
|
function flag(title, help, inner){ return '<div class="flag-card"><div class="flag-title">'+title+'</div><div class="flag-help">'+help+'</div>'+inner+'</div>'; }
|
||||||
|
function wgt(title, inner){ return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>'; }
|
||||||
|
function hero(ph,label,title,formula,desc,tags){ return '<div class="para-hero ph-'+ph+'"><div class="ph-label">'+label+'</div><h2>'+title+'</h2>'+(formula?'<div class="ph-formula">'+formula+'</div>':'')+'<div class="ph-desc">'+desc+'</div>'+(tags?'<div class="ph-tags">'+tags.map(function(t){return '<span class="ph-tag">'+t+'</span>';}).join('')+'</div>':'')+'</div>'; }
|
||||||
|
|
||||||
|
window.BUILDERS = { p46:bp46, p47:bp47, p48:bp48, p49:bp49, p50:bp50, p51:bp51, p52:bp52, final1:bfinal };
|
||||||
|
|
||||||
|
function bp46(){ document.getElementById('p46-body').innerHTML =
|
||||||
|
hero(7,'§ 46 · Глава 6','Смеси веществ','однород. / неоднород.','Большинство веществ вокруг — смеси. Их делят по тому, видны ли компоненты.',['смесь','разделение'])
|
||||||
|
+makeCard('theory','Виды смесей','§46','<p><b>Смесь</b> — несколько веществ вместе. <b>Однородные</b> смеси (растворы) — состав одинаков во всём объёме, частицы не видны (воздух, раствор соли, сплав). <b>Неоднородные</b> — компоненты различимы (песок + вода, молоко, гранит).</p><p>Смеси разделяют физическими способами: <b>фильтрование</b>, <b>отстаивание</b>, <b>выпаривание</b>, <b>перегонка</b>, действие магнитом.</p>')
|
||||||
|
+flag('Классификатор смесей','Распредели смеси на однородные и неоднородные.','<div id="c-mix"></div>')
|
||||||
|
+rememberBox(['Раствор — это однородная смесь.','Смеси разделяют физическими методами, без реакций.'])
|
||||||
|
+qList(['Чем раствор отличается от неоднородной смеси?','Как разделить смесь соли и песка?'])
|
||||||
|
+secNav(null,'p47')+readButton('p46'); wireReadBtn('p46'); }
|
||||||
|
|
||||||
|
function bp47(){ document.getElementById('p47-body').innerHTML =
|
||||||
|
hero(4,'§ 47 · Глава 6','Растворение веществ в воде','растворитель + в-во','Что происходит, когда сахар «исчезает» в чае: вода разбирает вещество на частицы.',['раствор','гидратация'])
|
||||||
|
+makeCard('theory','Как идёт растворение','§47','<p><b>Раствор</b> = растворитель (чаще вода) + растворённое вещество. При растворении молекулы воды окружают частицы вещества и «растаскивают» их — это <b>гидратация</b>. При этом тепло может выделяться (растворение щёлочи) или поглощаться (растворение соли).</p>')
|
||||||
|
+flag('Анимация растворения и гидратации','Выбери вещество — увидишь, как оно распадается на ионы, окружённые молекулами воды.','<div id="c-dissoc"></div>')
|
||||||
|
+rememberBox(['Раствор = растворитель + растворённое вещество.','Гидратация — окружение частиц молекулами воды.'])
|
||||||
|
+qList(['Из чего состоит любой раствор?','Что такое гидратация?'])
|
||||||
|
+secNav('p46','p48')+readButton('p47'); wireReadBtn('p47'); }
|
||||||
|
|
||||||
|
function bp48(){ document.getElementById('p48-body').innerHTML =
|
||||||
|
hero(8,'§ 48 · Глава 6','Характеристики растворимости веществ','s = f(t)','Сколько вещества можно растворить и как на это влияет температура.',['растворимость','кривая'])
|
||||||
|
+makeCard('theory','Растворимость','§48','<div class="def-box"><b>Растворимость</b> — масса вещества (в граммах), которая растворяется в 100 г воды при данной температуре до образования насыщенного раствора.</div><p>У большинства твёрдых веществ растворимость <b>растёт</b> с температурой (KNO₃), у некоторых почти не меняется (NaCl). Зависимость показывает <b>кривая растворимости</b>.</p>')
|
||||||
|
+flag('Кривая растворимости s = f(t)','Двигай температуру — точка идёт по кривой; сравни KNO₃ и NaCl.','<div id="c-solcurve"></div>')
|
||||||
|
+rememberBox(['Растворимость — г на 100 г воды.','У большинства твёрдых веществ растёт с t.'])
|
||||||
|
+qList(['Как меняется растворимость KNO₃ при нагревании?','Почему растворимость NaCl почти постоянна?'])
|
||||||
|
+secNav('p47','p49')+readButton('p48'); wireReadBtn('p48'); }
|
||||||
|
|
||||||
|
function bp49(){ document.getElementById('p49-body').innerHTML =
|
||||||
|
hero(5,'§ 49 · Глава 6','Качественные характеристики состава растворов','насыщ. / ненасыщ.','Как «на словах» описать раствор: много или мало вещества, можно ли растворить ещё.',['насыщенный','концентрированный'])
|
||||||
|
+makeCard('theory','Качественные характеристики','§49','<p><b>Ненасыщенный</b> раствор — может растворить ещё вещества. <b>Насыщенный</b> — достиг предела растворения при данной температуре. <b>Пересыщенный</b> — содержит больше вещества, чем в насыщенном (неустойчив).</p><p><b>Разбавленный</b> — мало растворённого вещества, <b>концентрированный</b> — много. Это <b>качественные</b> (приблизительные) характеристики; точные — массовая доля и концентрация.</p>')
|
||||||
|
+rememberBox(['Насыщенный — предел растворения при данной t.','«Разбавленный/концентрированный» — приблизительно.'])
|
||||||
|
+qList(['Чем насыщенный раствор отличается от ненасыщенного?','Можно ли в насыщенный раствор добавить ещё вещества?'])
|
||||||
|
+secNav('p48','p50')+readButton('p49'); wireReadBtn('p49'); }
|
||||||
|
|
||||||
|
function bp50(){ document.getElementById('p50-body').innerHTML =
|
||||||
|
hero(6,'§ 50 · Глава 6','Массовая доля растворённого вещества','w = m(в-ва)/m(р-ра)','Главная количественная характеристика раствора в быту и на этикетках.',['w','%'])
|
||||||
|
+makeCard('theory','Массовая доля','§50','<div class="def-box"><b>Массовая доля</b> w = m(растворённого вещества) / m(раствора). Часто выражают в процентах (× 100 %). Масса раствора = масса вещества + масса растворителя.</div><p>Пример: в 80 г воды растворили 20 г соли. m(р-ра) = 100 г, w = 20/100 = 0,2 = 20 %.</p>')
|
||||||
|
+flag('Калькулятор массовой доли','Введи массы вещества и воды — получишь w.','<div id="c-wcalc"></div>')
|
||||||
|
+rememberBox(['w = m(в-ва)/m(р-ра); m(р-ра) = m(в-ва)+m(воды).','w часто выражают в %.'])
|
||||||
|
+qList(['Найди w, если 30 г сахара растворили в 120 г воды.','Сколько соли в 200 г раствора с w = 10 %?'])
|
||||||
|
+secNav('p49','p51')+readButton('p50'); wireReadBtn('p50'); }
|
||||||
|
|
||||||
|
function bp51(){ document.getElementById('p51-body').innerHTML =
|
||||||
|
hero(9,'§ 51 · Глава 6','Молярная концентрация · ПР 4','c = n/V','Точная «химическая» характеристика раствора — сколько моль вещества в литре.',['c','моль/л','ПР.4'])
|
||||||
|
+makeCard('theory','Молярная концентрация','§51','<div class="def-box"><b>Молярная концентрация</b> c = n/V — химическое количество растворённого вещества в 1 литре раствора, моль/л.</div><p>Чтобы найти c по массе: сначала n = m/M, затем c = n/V. Пример: 40 г NaOH (M = 40) в 1 л → n = 1 моль, c = 1 моль/л.</p>')
|
||||||
|
+flag('Калькулятор молярной концентрации','Введи формулу, массу и объём — получишь c = n/V.','<div id="c-ccalc"></div>')
|
||||||
|
+makeCard('lab','Практическая работа 4 · Приготовление раствора',null,'<p>Приготовь раствор с заданной массовой долей и молярной концентрацией: рассчитай нужную массу вещества, взвесь её, раствори в воде и доведи до нужного объёма в мерной колбе.</p>')
|
||||||
|
+rememberBox(['c = n/V (моль/л); сначала найди n = m/M.','Для приготовления раствора используют мерную колбу.'])
|
||||||
|
+qList(['Найди c, если 0,5 моль вещества в 2 л раствора.','Какую массу NaCl (M=58,5) нужно для 0,5 л раствора c = 1 моль/л?'])
|
||||||
|
+secNav('p50','p52')+readButton('p51'); wireReadBtn('p51'); }
|
||||||
|
|
||||||
|
function bp52(){ document.getElementById('p52-body').innerHTML =
|
||||||
|
hero(7,'§ 52 · Глава 6','Вода и растворы в жизни человека','H₂O','Вода — основа жизни и самый важный растворитель на планете.',['вода','очистка','значение'])
|
||||||
|
+makeCard('theory','Значение воды','§52','<p>Вода — <b>универсальный растворитель</b>: в ней растворяются соли, газы, питательные вещества. Растворы — это кровь, морская вода, почвенный раствор, напитки, лекарства.</p><p>Питьевую воду <b>очищают</b>: отстаиванием, фильтрованием, обеззараживанием, кипячением. Беречь чистую воду — задача каждого.</p>')
|
||||||
|
+'<div class="lat-grid"><div class="lat-card"><h4>В организме</h4><ul><li>кровь, лимфа — растворы</li><li>перенос веществ</li></ul></div><div class="lat-card"><h4>В природе</h4><ul><li>морская вода</li><li>почвенный раствор</li></ul></div><div class="lat-card"><h4>В быту</h4><ul><li>напитки, чай</li><li>лекарства</li></ul></div><div class="lat-card"><h4>Очистка</h4><ul><li>фильтрование</li><li>кипячение</li></ul></div></div>'
|
||||||
|
+rememberBox(['Вода — универсальный растворитель и основа жизни.','Воду очищают фильтрованием, отстаиванием, кипячением.'])
|
||||||
|
+qList(['Почему воду называют универсальным растворителем?','Назови способы очистки воды.'])
|
||||||
|
+secNav('p51','final1')+readButton('p52'); wireReadBtn('p52'); }
|
||||||
|
|
||||||
|
function bfinal(){ document.getElementById('final1-body').innerHTML =
|
||||||
|
hero('final','Финал главы 6','Босс: растворы','смеси · растворимость · w · c','Шесть интегрированных задач по всей главе. Победи босса — ачивка «Растворы освоены».')
|
||||||
|
+makeCard('rule','Шпаргалка главы',null,'<div class="formula-grid"><div class="fcard"><h3>Массовая доля</h3><div class="main-f">w = m(в-ва)/m(р-ра)</div></div><div class="fcard"><h3>Концентрация</h3><div class="main-f">c = n/V</div></div><div class="fcard"><h3>Растворимость</h3><div class="main-f">г / 100 г воды</div></div><div class="fcard highlight"><h3>Смеси</h3><div class="main-f">однород./неоднород.</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши все задачи — за каждую +5 XP, за победу — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p52',null); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,622 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 класс — учебник</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#fffbeb; --card:#fff;
|
||||||
|
--text:#1c1917; --muted:#78716c;
|
||||||
|
--border:#fde68a;
|
||||||
|
--pri:#d97706; --pri-d:#b45309;
|
||||||
|
--pri-soft:#fef3c7;
|
||||||
|
--c0:#d97706; --c0-d:#b45309; /* intro — amber */
|
||||||
|
--c1:#0d9488; --c1-d:#0f766e; /* гл.1 — teal */
|
||||||
|
--c2:#4f46e5; --c2-d:#4338ca; /* гл.2 — indigo */
|
||||||
|
--c3:#2563eb; --c3-d:#1d4ed8; /* гл.3 — blue */
|
||||||
|
--c4:#059669; --c4-d:#047857; /* гл.4 — green */
|
||||||
|
--c5:#ea580c; --c5-d:#c2410c; /* гл.5 — orange */
|
||||||
|
--c6:#0891b2; --c6-d:#0e7490; /* гл.6 — cyan */
|
||||||
|
--sh:0 4px 16px rgba(217,119,6,.10);
|
||||||
|
--sh-h:0 12px 36px rgba(217,119,6,.18);
|
||||||
|
}
|
||||||
|
html.dark{
|
||||||
|
--bg:#1c1410; --card:#271c14;
|
||||||
|
--text:#fef3c7; --muted:#d6b88a;
|
||||||
|
--border:#4a3520;
|
||||||
|
--pri-soft:rgba(217,119,6,.16);
|
||||||
|
}
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{min-height:100vh}
|
||||||
|
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;transition:background .25s,color .25s}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
.hdr{position:relative;background:linear-gradient(110deg,#92400e 0%,#d97706 55%,#fbbf24 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(254,243,199,.18)}
|
||||||
|
.hdr::before{content:'ХИМИЯ';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(254,243,199,.14);line-height:1;pointer-events:none;user-select:none}
|
||||||
|
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
|
||||||
|
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.16);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
|
||||||
|
.hdr-back:hover{background:rgba(255,255,255,.26)}
|
||||||
|
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
|
||||||
|
.hdr-sub{font-size:.92rem;opacity:.9;margin-top:4px}
|
||||||
|
.hdr-side{margin-left:auto;display:flex;gap:8px}
|
||||||
|
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit}
|
||||||
|
.hdr-btn:hover{background:rgba(255,255,255,.26)}
|
||||||
|
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
|
||||||
|
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||||
|
|
||||||
|
/* OVERALL PROGRESS */
|
||||||
|
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.12));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
|
||||||
|
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#d97706,#fbbf24);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
|
||||||
|
.po-text{flex:1;min-width:160px}
|
||||||
|
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
|
||||||
|
.po-bar{height:8px;background:rgba(217,119,6,.16);border-radius:5px;overflow:hidden;margin-top:6px}
|
||||||
|
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#fbbf24);border-radius:5px;transition:width .5s}
|
||||||
|
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#f59e0b,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif;letter-spacing:.02em;box-shadow:0 4px 12px rgba(217,119,6,.24)}
|
||||||
|
|
||||||
|
/* CHAPTER GRID */
|
||||||
|
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
|
||||||
|
@media(min-width:680px){.ch-grid{grid-template-columns:1fr 1fr}}
|
||||||
|
@media(min-width:1000px){.ch-grid{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
|
||||||
|
.ch-card{background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s,border-color .2s;cursor:pointer;text-decoration:none;color:inherit}
|
||||||
|
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
|
||||||
|
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden}
|
||||||
|
.ch-cover-wm{position:absolute;right:-4px;top:-14px;font-size:4rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.22);pointer-events:none;letter-spacing:-.03em}
|
||||||
|
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.24);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
|
||||||
|
.ch-title{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
|
||||||
|
.ch-range{font-size:.84rem;opacity:.9;margin-top:4px;position:relative;z-index:1;font-weight:500}
|
||||||
|
|
||||||
|
.ch-cover.cc0{background:linear-gradient(135deg,#92400e,#d97706 60%,#fbbf24)}
|
||||||
|
.ch-cover.cc1{background:linear-gradient(135deg,#134e4a,#0d9488 60%,#14b8a6)}
|
||||||
|
.ch-cover.cc2{background:linear-gradient(135deg,#3730a3,#4f46e5 60%,#818cf8)}
|
||||||
|
.ch-cover.cc3{background:linear-gradient(135deg,#1e3a8a,#2563eb 60%,#60a5fa)}
|
||||||
|
.ch-cover.cc4{background:linear-gradient(135deg,#064e3b,#059669 60%,#34d399)}
|
||||||
|
.ch-cover.cc5{background:linear-gradient(135deg,#9a3412,#ea580c 60%,#fb923c)}
|
||||||
|
.ch-cover.cc6{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
|
||||||
|
|
||||||
|
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
|
||||||
|
.ch-desc{font-size:.88rem;color:var(--text);opacity:.84;flex:1;margin-bottom:12px;line-height:1.55}
|
||||||
|
|
||||||
|
.ch-prog{margin-bottom:12px}
|
||||||
|
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
|
||||||
|
.ch-prog-bar{height:6px;background:rgba(0,0,0,.07);border-radius:4px;overflow:hidden}
|
||||||
|
.ch-prog-fill{height:100%;border-radius:4px;transition:width .5s}
|
||||||
|
.ch-card.k0 .ch-prog-fill{background:linear-gradient(90deg,var(--c0),var(--c0-d))}
|
||||||
|
.ch-card.k1 .ch-prog-fill{background:linear-gradient(90deg,var(--c1),var(--c1-d))}
|
||||||
|
.ch-card.k2 .ch-prog-fill{background:linear-gradient(90deg,var(--c2),var(--c2-d))}
|
||||||
|
.ch-card.k3 .ch-prog-fill{background:linear-gradient(90deg,var(--c3),var(--c3-d))}
|
||||||
|
.ch-card.k4 .ch-prog-fill{background:linear-gradient(90deg,var(--c4),var(--c4-d))}
|
||||||
|
.ch-card.k5 .ch-prog-fill{background:linear-gradient(90deg,var(--c5),var(--c5-d))}
|
||||||
|
.ch-card.k6 .ch-prog-fill{background:linear-gradient(90deg,var(--c6),var(--c6-d))}
|
||||||
|
|
||||||
|
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.9rem;color:#fff;transition:filter .15s}
|
||||||
|
.ch-action:hover{filter:brightness(1.08)}
|
||||||
|
.ch-card.k0 .ch-action{background:linear-gradient(135deg,var(--c0),#fbbf24)}
|
||||||
|
.ch-card.k1 .ch-action{background:linear-gradient(135deg,var(--c1),#14b8a6)}
|
||||||
|
.ch-card.k2 .ch-action{background:linear-gradient(135deg,var(--c2),#818cf8)}
|
||||||
|
.ch-card.k3 .ch-action{background:linear-gradient(135deg,var(--c3),#60a5fa)}
|
||||||
|
.ch-card.k4 .ch-action{background:linear-gradient(135deg,var(--c4),#34d399)}
|
||||||
|
.ch-card.k5 .ch-action{background:linear-gradient(135deg,var(--c5),#fb923c)}
|
||||||
|
.ch-card.k6 .ch-action{background:linear-gradient(135deg,var(--c6),#22d3ee)}
|
||||||
|
|
||||||
|
/* COURSE FINAL (placeholder — наполняется в Phase 7) */
|
||||||
|
.final-wrap{margin:0 0 28px;background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:var(--sh)}
|
||||||
|
.final-head{padding:18px 22px;background:linear-gradient(135deg,#92400e 0%,#d97706 55%,#f59e0b 100%);color:#fff;cursor:pointer;display:flex;align-items:center;gap:14px;user-select:none;transition:filter .15s}
|
||||||
|
.final-head:hover{filter:brightness(1.06)}
|
||||||
|
.final-head-icon{width:46px;height:46px;border-radius:12px;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.final-head-icon svg{width:26px;height:26px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.final-head-text{flex:1;min-width:0}
|
||||||
|
.final-head-tag{display:inline-block;padding:3px 9px;background:rgba(255,255,255,.24);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
|
||||||
|
.final-head-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;letter-spacing:-.01em;line-height:1.25}
|
||||||
|
.final-head-sub{font-size:.84rem;opacity:.9;margin-top:2px}
|
||||||
|
.final-chevron{flex-shrink:0;transition:transform .25s}
|
||||||
|
.final-chevron svg{width:24px;height:24px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.final-wrap.open .final-chevron{transform:rotate(180deg)}
|
||||||
|
.final-body{display:none;padding:22px}
|
||||||
|
.final-wrap.open .final-body{display:block}
|
||||||
|
.fin-placeholder{padding:24px 18px;background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.08));border:1.5px dashed var(--pri);border-radius:14px;text-align:center;color:var(--text)}
|
||||||
|
.fin-placeholder h3{font-family:'Outfit',sans-serif;color:var(--pri-d);margin-bottom:8px;font-size:1.1rem}
|
||||||
|
.fin-placeholder p{color:var(--muted);font-size:.92rem;line-height:1.55}
|
||||||
|
.fin-section-title{font-family:'Outfit',sans-serif;font-size:1.12rem;font-weight:800;color:var(--text);margin:8px 0 14px;display:flex;align-items:center;gap:9px}
|
||||||
|
.fin-section-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
|
||||||
|
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
|
||||||
|
@media(min-width:1000px){.cheat-grid{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
.cheat-card{border:1.5px solid var(--border);border-radius:13px;padding:14px 16px;background:var(--card);position:relative;overflow:hidden}
|
||||||
|
.cheat-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px}
|
||||||
|
.cheat-card.c1::before{background:#d97706}.cheat-card.c2::before{background:#0d9488}.cheat-card.c3::before{background:#4f46e5}.cheat-card.c4::before{background:#2563eb}.cheat-card.c5::before{background:#059669}.cheat-card.c6::before{background:#ea580c}.cheat-card.c7::before{background:#0891b2}
|
||||||
|
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
|
||||||
|
.cheat-badge{font-size:.68rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.04em;text-transform:uppercase}
|
||||||
|
.cheat-card.c1 .cheat-badge{background:#d97706}.cheat-card.c2 .cheat-badge{background:#0d9488}.cheat-card.c3 .cheat-badge{background:#4f46e5}.cheat-card.c4 .cheat-badge{background:#2563eb}.cheat-card.c5 .cheat-badge{background:#059669}.cheat-card.c6 .cheat-badge{background:#ea580c}.cheat-card.c7 .cheat-badge{background:#0891b2}
|
||||||
|
.cheat-title{font-weight:800;color:var(--text);font-size:.96rem}
|
||||||
|
.cheat-list{list-style:none;padding-left:6px;margin:0}
|
||||||
|
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.9rem;line-height:1.5;color:var(--text)}
|
||||||
|
.cheat-list li:last-child{border-bottom:0}
|
||||||
|
.boss-overall-bar{background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.08));border:1px solid var(--border);border-radius:12px;padding:13px 16px;margin:6px 0 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
|
||||||
|
.boss-overall-bar .lab{font-weight:700;font-size:.95rem;color:var(--text);min-width:200px}
|
||||||
|
.boss-overall-bar .bar{flex:1;min-width:160px;height:9px;background:rgba(217,119,6,.16);border-radius:5px;overflow:hidden}
|
||||||
|
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,var(--pri),#fbbf24);transition:width .5s;border-radius:5px}
|
||||||
|
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s}
|
||||||
|
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
|
||||||
|
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
|
||||||
|
.boss-tag{font-size:.68rem;font-weight:800;padding:3px 9px;border-radius:99px;background:var(--pri-soft);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
|
||||||
|
html.dark .boss-tag{color:var(--pri-l)}
|
||||||
|
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1rem;flex:1;min-width:0}
|
||||||
|
.boss-q{padding:12px 14px;background:var(--pri-soft);border-radius:10px;font-size:.95rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
|
||||||
|
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
|
||||||
|
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem}
|
||||||
|
.boss-input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||||
|
.boss-btn{padding:8px 16px;border-radius:9px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:.15s}
|
||||||
|
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||||||
|
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#fbbf24);color:#fff;border-color:transparent}
|
||||||
|
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
|
||||||
|
.boss-fb.ok{display:block;background:#d1fae5;color:#065f46;border-left:4px solid #10b981}
|
||||||
|
.boss-fb.fail{display:block;background:#fee2e2;color:#7f1d1d;border-left:4px solid #dc2626}
|
||||||
|
html.dark .boss-fb.ok{background:rgba(16,185,129,.18);color:#a7f3d0}html.dark .boss-fb.fail{background:rgba(220,38,38,.18);color:#fecaca}
|
||||||
|
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
|
||||||
|
.boss-hint-txt.show{display:block}
|
||||||
|
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:1.5px solid #fbbf24;display:none;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
|
.final-cta.show{display:flex}
|
||||||
|
html.dark .final-cta{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(217,119,6,.15));border-color:#d97706}
|
||||||
|
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#fbbf24,#f59e0b);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.final-cta-icon svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.final-cta-txt{flex:1;min-width:180px}
|
||||||
|
.final-cta-title{font-weight:800;color:#92400e;font-size:1.05rem;font-family:'Outfit',sans-serif}
|
||||||
|
html.dark .final-cta-title{color:#fde68a}
|
||||||
|
.final-cta-sub{font-size:.86rem;color:#78350f;margin-top:2px}html.dark .final-cta-sub{color:#fcd34d}
|
||||||
|
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#f59e0b);color:#fff;text-decoration:none;font-weight:800;font-size:.9rem;display:inline-flex;align-items:center;gap:7px;transition:filter .15s}
|
||||||
|
.final-cta-btn:hover{filter:brightness(1.1)}
|
||||||
|
.final-cta-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
|
||||||
|
/* ACHIEVEMENT STRIP */
|
||||||
|
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
|
||||||
|
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
|
||||||
|
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
|
||||||
|
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
|
||||||
|
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.ach-strip.lit .ach-icon svg{stroke:#fff}
|
||||||
|
.ach-text{flex:1}
|
||||||
|
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
|
||||||
|
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
|
||||||
|
.ach-strip.lit .ach-title{color:#92400e}
|
||||||
|
html.dark .ach-strip.lit .ach-title{color:#fde68a}
|
||||||
|
|
||||||
|
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-inner">
|
||||||
|
<div>
|
||||||
|
<a href="/textbooks" class="hdr-back">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
|
||||||
|
К каталогу
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>Химия — 8 класс</h1>
|
||||||
|
<div class="hdr-sub">Количественные понятия, классы соединений, периодический закон, строение атома, химическая связь, ОВР, растворы. 7 разделов, 52 параграфа.</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
|
||||||
|
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
||||||
|
<span id="theme-lab">Тёмная</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<section class="prog-overall">
|
||||||
|
<div class="po-icon">Х</div>
|
||||||
|
<div class="po-text">
|
||||||
|
<div class="po-label">Общий прогресс по курсу</div>
|
||||||
|
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||||
|
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="ch-grid">
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-intro" class="ch-card k0" id="ch-1">
|
||||||
|
<div class="ch-cover cc0">
|
||||||
|
<div class="ch-cover-wm">mol</div>
|
||||||
|
<div class="ch-num">Вводный раздел</div>
|
||||||
|
<div class="ch-title">Количественные понятия в химии</div>
|
||||||
|
<div class="ch-range">§1–§9 · ПР 1</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Атомы и элементы, простые и сложные вещества, химическое количество вещества, моль и постоянная Авогадро, молярная масса и объём газов, расчёты по массе, объёму и уравнениям реакций.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-1">Открыть раздел</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch1" class="ch-card k1" id="ch-2">
|
||||||
|
<div class="ch-cover cc1">
|
||||||
|
<div class="ch-cover-wm">OH−</div>
|
||||||
|
<div class="ch-num">Глава 1</div>
|
||||||
|
<div class="ch-title">Важнейшие классы неорганических соединений</div>
|
||||||
|
<div class="ch-range">§10–§23 · 2 лаб · ПР 2,3</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Оксиды, кислоты, основания и соли: состав, классификация, химические свойства, получение и применение; генетическая связь между классами неорганических веществ.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-2">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch2" class="ch-card k2" id="ch-3">
|
||||||
|
<div class="ch-cover cc2">
|
||||||
|
<div class="ch-cover-wm">№</div>
|
||||||
|
<div class="ch-num">Глава 2</div>
|
||||||
|
<div class="ch-title">Периодический закон и периодическая система</div>
|
||||||
|
<div class="ch-range">§24–§28 · 1 лаб</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Систематизация элементов, амфотерность, естественные семейства элементов, периодический закон Д. И. Менделеева и строение периодической системы.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-3">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch3" class="ch-card k3" id="ch-4">
|
||||||
|
<div class="ch-cover cc3">
|
||||||
|
<div class="ch-cover-wm">e−</div>
|
||||||
|
<div class="ch-num">Глава 3</div>
|
||||||
|
<div class="ch-title">Строение атома</div>
|
||||||
|
<div class="ch-range">§29–§35</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Строение атома и атомный номер, массовое число и нуклиды, изотопы и радиоактивность, электронное облако и орбиталь, электронные оболочки, периодичность свойств.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-4">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-4" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-4">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch4" class="ch-card k4" id="ch-5">
|
||||||
|
<div class="ch-cover cc4">
|
||||||
|
<div class="ch-cover-wm">H₂O</div>
|
||||||
|
<div class="ch-num">Глава 4</div>
|
||||||
|
<div class="ch-title">Химическая связь</div>
|
||||||
|
<div class="ch-range">§36–§41 · 1 лаб</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Природа химической связи, ковалентная связь (неполярная и полярная, электроотрицательность), ионная и металлическая связь, межмолекулярное взаимодействие, кристаллические решётки.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-5">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-5" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-5">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch5" class="ch-card k5" id="ch-6">
|
||||||
|
<div class="ch-cover cc5">
|
||||||
|
<div class="ch-cover-wm">O₂</div>
|
||||||
|
<div class="ch-num">Глава 5</div>
|
||||||
|
<div class="ch-title">Окислительно-восстановительные реакции</div>
|
||||||
|
<div class="ch-range">§42–§45</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Степень окисления, процессы окисления и восстановления, окислительно-восстановительные реакции и метод электронного баланса, ОВР вокруг нас.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-6">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-6" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-6">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/textbook/chemistry-8-ch6" class="ch-card k6" id="ch-7">
|
||||||
|
<div class="ch-cover cc6">
|
||||||
|
<div class="ch-cover-wm">aq</div>
|
||||||
|
<div class="ch-num">Глава 6</div>
|
||||||
|
<div class="ch-title">Растворы</div>
|
||||||
|
<div class="ch-range">§46–§52 · ПР 4</div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-body">
|
||||||
|
<div class="ch-desc">Смеси веществ, растворение в воде, характеристики растворимости, качественные и количественные характеристики состава растворов, массовая доля и молярная концентрация, вода в жизни человека.</div>
|
||||||
|
<div class="ch-prog">
|
||||||
|
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-7">0%</span></div>
|
||||||
|
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-7" style="width:0%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="ch-action"><span id="btn-7">Открыть главу</span><svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="final-wrap" id="course-final">
|
||||||
|
<div class="final-head" id="final-head" tabindex="0" role="button" aria-expanded="false" aria-controls="final-body">
|
||||||
|
<div class="final-head-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M7 4h10v6a5 5 0 0 1-10 0V4z"/><path d="M5 4h2v2H5a2 2 0 0 1 0-4M19 4h-2v2h2a2 2 0 0 0 0-4M9 20h6M12 15v5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="final-head-text">
|
||||||
|
<div class="final-head-tag">Финал курса</div>
|
||||||
|
<div class="final-head-title">Босс-проверка по всему курсу</div>
|
||||||
|
<div class="final-head-sub">Шпаргалка курса и интегрированные боссы по всем 7 разделам. Победи всех — получи «Химик 8 класса».</div>
|
||||||
|
</div>
|
||||||
|
<div class="final-chevron"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></div>
|
||||||
|
</div>
|
||||||
|
<div class="final-body" id="final-body">
|
||||||
|
|
||||||
|
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg> Шпаргалка курса</div>
|
||||||
|
<div class="cheat-grid">
|
||||||
|
<div class="cheat-card c1"><div class="cheat-head"><span class="cheat-badge">Вводный</span><span class="cheat-title">Количество вещества</span></div><ul class="cheat-list"><li>$n=\dfrac{m}{M}$, $\;M=M_r$</li><li>$V=n\cdot22{,}4$ л/моль (н.у.)</li><li>$N=n\cdot6{,}02\cdot10^{23}$</li><li>Расчёт по уравнению — по коэффициентам</li></ul></div>
|
||||||
|
<div class="cheat-card c2"><div class="cheat-head"><span class="cheat-badge">Гл. 1</span><span class="cheat-title">Классы соединений</span></div><ul class="cheat-list"><li>Оксиды: осн./кисл./амфот.</li><li>Кислоты: основность = число H</li><li>Основания: щёлочи / нераств.</li><li>Соль + щёлочь/кислота/Me (РИО)</li></ul></div>
|
||||||
|
<div class="cheat-card c3"><div class="cheat-head"><span class="cheat-badge">Гл. 2</span><span class="cheat-title">Периодический закон</span></div><ul class="cheat-list"><li>Период = число слоёв</li><li>Группа = внешние электроны</li><li>Амфотерность: Zn(OH)₂, Al(OH)₃</li><li>Семейства: щелочные, галогены</li></ul></div>
|
||||||
|
<div class="cheat-card c4"><div class="cheat-head"><span class="cheat-badge">Гл. 3</span><span class="cheat-title">Строение атома</span></div><ul class="cheat-list"><li>$A=Z+N$; $Z=p^+=e^-$</li><li>Изотопы — разный N</li><li>Слой: $2n^2$ электронов</li><li>Свойства — внешний слой</li></ul></div>
|
||||||
|
<div class="cheat-card c5"><div class="cheat-head"><span class="cheat-badge">Гл. 4</span><span class="cheat-title">Химическая связь</span></div><ul class="cheat-list"><li>Ковалентная — общие пары</li><li>Ионная — передача e⁻</li><li>Металлическая — электронный газ</li><li>Решётка → свойства</li></ul></div>
|
||||||
|
<div class="cheat-card c6"><div class="cheat-head"><span class="cheat-badge">Гл. 5</span><span class="cheat-title">ОВР</span></div><ul class="cheat-list"><li>С.о.: H +1, O −2, Σ=0</li><li>Окисление −e⁻; восстановление +e⁻</li><li>Баланс: отдано = принято e⁻</li></ul></div>
|
||||||
|
<div class="cheat-card c7"><div class="cheat-head"><span class="cheat-badge">Гл. 6</span><span class="cheat-title">Растворы</span></div><ul class="cheat-list"><li>$w=\dfrac{m_{в-ва}}{m_{р-ра}}$</li><li>$c=\dfrac{n}{V}$ (моль/л)</li><li>Растворимость: г / 100 г воды</li><li>Смеси: однород./неоднород.</li></ul></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M14.5 3.5l-5 5L4 4l1.5 6L3 12l5 1 1 5 2.5-2.5 6 1.5-4.5-5.5 5-5"/></svg> 10 интегрированных боссов</div>
|
||||||
|
<div class="boss-overall-bar"><div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 10</div><div class="bar"><div class="fill" id="fin-boss-fill" style="width:0%"></div></div></div>
|
||||||
|
<div id="fin-bosses-container"></div>
|
||||||
|
|
||||||
|
<div class="final-cta" id="final-cta">
|
||||||
|
<div class="final-cta-icon"><svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg></div>
|
||||||
|
<div class="final-cta-txt"><div class="final-cta-title">Курс «Химия 8» пройден!</div><div class="final-cta-sub">Вы прошли итоговую проверку по всем 7 разделам. +150 XP, ачивка «Химик 8 класса» получена.</div></div>
|
||||||
|
<a href="/textbooks" class="final-cta-btn">К каталогу <svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="ach-strip" id="ach-strip">
|
||||||
|
<div class="ach-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="ach-text">
|
||||||
|
<div class="ach-title">Химик 8 класса</div>
|
||||||
|
<div class="ach-sub" id="ach-sub">Изучите все 52 параграфа курса, чтобы получить достижение.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
Интерактивный учебник «Химия — 8 класс» · Шиманович, Красицкий, Сечко, Хвалюк · LearnSpace
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* THEME */
|
||||||
|
(function(){
|
||||||
|
var saved = localStorage.getItem('chemistry8_theme') || localStorage.getItem('theme') || 'light';
|
||||||
|
if (saved === 'dark') document.documentElement.classList.add('dark');
|
||||||
|
var lab = document.getElementById('theme-lab');
|
||||||
|
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
|
||||||
|
document.getElementById('theme-btn').addEventListener('click', function(){
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
var dark = document.documentElement.classList.contains('dark');
|
||||||
|
localStorage.setItem('chemistry8_theme', dark ? 'dark' : 'light');
|
||||||
|
localStorage.setItem('theme', dark ? 'dark' : 'light');
|
||||||
|
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* PROGRESS */
|
||||||
|
var TOTAL = 52;
|
||||||
|
var CH_PARA = {
|
||||||
|
'chemistry-8-intro': 9,
|
||||||
|
'chemistry-8-ch1': 14,
|
||||||
|
'chemistry-8-ch2': 5,
|
||||||
|
'chemistry-8-ch3': 7,
|
||||||
|
'chemistry-8-ch4': 6,
|
||||||
|
'chemistry-8-ch5': 4,
|
||||||
|
'chemistry-8-ch6': 7
|
||||||
|
};
|
||||||
|
var CH_IDX = {
|
||||||
|
'chemistry-8-intro': 1,
|
||||||
|
'chemistry-8-ch1': 2,
|
||||||
|
'chemistry-8-ch2': 3,
|
||||||
|
'chemistry-8-ch3': 4,
|
||||||
|
'chemistry-8-ch4': 5,
|
||||||
|
'chemistry-8-ch5': 6,
|
||||||
|
'chemistry-8-ch6': 7
|
||||||
|
};
|
||||||
|
|
||||||
|
function setChProg(idx, readCount, total) {
|
||||||
|
var pct = total ? Math.round(readCount * 100 / total) : 0;
|
||||||
|
var labelEl = document.getElementById('prog-' + idx);
|
||||||
|
var fillEl = document.getElementById('fill-' + idx);
|
||||||
|
var btnEl = document.getElementById('btn-' + idx);
|
||||||
|
if (labelEl) labelEl.textContent = pct + '%';
|
||||||
|
if (fillEl) fillEl.style.width = pct + '%';
|
||||||
|
if (btnEl) {
|
||||||
|
var base = idx === 1 ? 'раздел' : 'главу';
|
||||||
|
if (readCount > 0 && readCount < total) btnEl.textContent = 'Продолжить';
|
||||||
|
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
|
||||||
|
else btnEl.textContent = 'Открыть ' + base;
|
||||||
|
}
|
||||||
|
return pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
var FIN_ACH_KEY = 'chemistry8_course_master';
|
||||||
|
|
||||||
|
function renderProgress(children) {
|
||||||
|
var totalRead = 0;
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
var ch = children[i];
|
||||||
|
var idx = CH_IDX[ch.slug];
|
||||||
|
if (!idx) continue;
|
||||||
|
var read = ch.progress ? ch.progress.read.length : 0;
|
||||||
|
var total = ch.para_count || CH_PARA[ch.slug] || 1;
|
||||||
|
totalRead += read;
|
||||||
|
setChProg(idx, read, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pct = Math.round(totalRead * 100 / TOTAL);
|
||||||
|
var overallEl = document.getElementById('overall-text');
|
||||||
|
var fillEl = document.getElementById('overall-fill');
|
||||||
|
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' параграфов \xb7 ' + pct + '%';
|
||||||
|
if (fillEl) fillEl.style.width = pct + '%';
|
||||||
|
|
||||||
|
var xpBadge = document.getElementById('hero-xp-badge');
|
||||||
|
var xp = parseInt(localStorage.getItem('chemistry8_xp') || '0', 10) || 0;
|
||||||
|
if (xpBadge && xp > 0) {
|
||||||
|
xpBadge.style.display = '';
|
||||||
|
xpBadge.textContent = xp + ' XP';
|
||||||
|
}
|
||||||
|
|
||||||
|
var mastered = localStorage.getItem(FIN_ACH_KEY) === '1';
|
||||||
|
if (totalRead >= TOTAL || mastered) {
|
||||||
|
var strip = document.getElementById('ach-strip');
|
||||||
|
var sub = document.getElementById('ach-sub');
|
||||||
|
if (strip) strip.classList.add('lit');
|
||||||
|
if (sub) sub.textContent = mastered
|
||||||
|
? 'Выполнено! Вы — Химик 8 класса.'
|
||||||
|
: 'Выполнено! Вы изучили весь курс химии 8 класса.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ФИНАЛ КУРСА: 10 интегрированных боссов ===== */
|
||||||
|
var FIN_BOSS_KEY = 'chemistry8_course_bosses';
|
||||||
|
var FIN_BOSSES = [
|
||||||
|
{ n:1, tag:'Вводный', title:'Относительная молекулярная масса', q:'Чему равна $M_r(\\text{Ca(OH)}_2)$?', hint:'$40 + 2\\cdot(16+1) = 74$.', ans:74 },
|
||||||
|
{ n:2, tag:'Вводный', title:'Количество вещества', q:'Сколько моль в $49$ г $\\text{H}_2\\text{SO}_4$ ($M=98$)?', hint:'$n=m/M=49/98=0{,}5$.', ans:0.5, tol:0.02, step:'0.01' },
|
||||||
|
{ n:3, tag:'Вводный + Гл.1', title:'Расчёт по уравнению', q:'$\\text{CaCO}_3 \\to \\text{CaO} + \\text{CO}_2$. Дано $m(\\text{CaCO}_3)=50$ г ($M=100$). Какой объём $\\text{CO}_2$ (л, н.у.)?', hint:'$n=0{,}5$ моль → $V=0{,}5\\cdot22{,}4=11{,}2$ л.', ans:11.2, tol:0.1, step:'0.1' },
|
||||||
|
{ n:4, tag:'Гл.1', title:'Качественная реакция', q:'В реакции $\\text{BaCl}_2+\\text{Na}_2\\text{SO}_4$ выпадает осадок $\\text{BaSO}_4$. Чему равна его $M_r$?', hint:'$137+32+4\\cdot16=233$.', ans:233 },
|
||||||
|
{ n:5, tag:'Гл.1', title:'Ряд активности', q:'Сколько из металлов Cu, Zn, Fe, Ag вытесняют $\\text{H}_2$ из соляной кислоты?', hint:'До водорода стоят Zn и Fe → 2.', ans:2 },
|
||||||
|
{ n:6, tag:'Гл.2', title:'Периодическая система', q:'Номер группы хлора (число внешних электронов)?', hint:'Cl — VII группа → 7.', ans:7 },
|
||||||
|
{ n:7, tag:'Гл.3', title:'Строение атома', q:'Сколько нейтронов в атоме $^{39}\\text{K}$ ($Z=19$)?', hint:'$N=A-Z=39-19=20$.', ans:20 },
|
||||||
|
{ n:8, tag:'Гл.4 + 5', title:'Степень окисления', q:'Чему равна степень окисления серы в $\\text{H}_2\\text{SO}_4$?', hint:'$2\\cdot(+1)+S+4\\cdot(-2)=0 \\Rightarrow S=+6$.', ans:6 },
|
||||||
|
{ n:9, tag:'Гл.5', title:'Электронный баланс', q:'Сколько электронов отдаёт алюминий при окислении $\\text{Al}^0 \\to \\text{Al}^{+3}$?', hint:'3 электрона.', ans:3 },
|
||||||
|
{ n:10, tag:'Гл.6', title:'Массовая доля', q:'В $80$ г воды растворили $20$ г соли. Чему равна массовая доля (%)?', hint:'$w=20/(20+80)\\cdot100=20\\%$.', ans:20 }
|
||||||
|
];
|
||||||
|
function loadFinBossState(){ try{ return JSON.parse(localStorage.getItem(FIN_BOSS_KEY)||'{}')||{}; }catch(e){ return {}; } }
|
||||||
|
function saveFinBossState(s){ try{ localStorage.setItem(FIN_BOSS_KEY, JSON.stringify(s)); }catch(e){} }
|
||||||
|
function finRenderKatex(root){ if(typeof window.renderMathInElement!=='function')return; try{ window.renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}); }catch(e){} }
|
||||||
|
function updateFinBossBar(state){ var won=0; for(var k in state) if(state[k])won++; var lab=document.getElementById('fin-boss-lab'),fill=document.getElementById('fin-boss-fill'); if(lab)lab.textContent='Боссов побеждено: '+won+' / '+FIN_BOSSES.length; if(fill)fill.style.width=Math.round(won*100/FIN_BOSSES.length)+'%'; return won; }
|
||||||
|
function maybeUnlockMaster(state){
|
||||||
|
if(localStorage.getItem(FIN_ACH_KEY)==='1')return;
|
||||||
|
var won=0; for(var k in state) if(state[k])won++; if(won<FIN_BOSSES.length)return;
|
||||||
|
localStorage.setItem(FIN_ACH_KEY,'1');
|
||||||
|
var xp=parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0; localStorage.setItem('chemistry8_xp',String(xp+150));
|
||||||
|
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(150,'chemistry8-master'); }catch(e){}
|
||||||
|
try{ if(window.confetti) window.confetti({particleCount:220,spread:110,origin:{y:.6}}); }catch(e){}
|
||||||
|
var strip=document.getElementById('ach-strip'),sub=document.getElementById('ach-sub');
|
||||||
|
if(strip)strip.classList.add('lit'); if(sub)sub.textContent='Выполнено! Вы — Химик 8 класса.';
|
||||||
|
var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show');
|
||||||
|
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0)+' XP'; }
|
||||||
|
}
|
||||||
|
function buildFinBoss(b,state){
|
||||||
|
var solved=!!state[b.n], step=b.step||'1';
|
||||||
|
var dispAns=(typeof b.ans==='number'&&step!=='1')?b.ans:b.ans;
|
||||||
|
return '<div class="boss-card'+(solved?' solved':'')+'" id="fb-'+b.n+'-card">'
|
||||||
|
+'<div class="boss-head"><span class="boss-tag">'+b.tag+'</span><span class="boss-title">Босс '+b.n+'. '+b.title+'</span></div>'
|
||||||
|
+'<div class="boss-q">'+b.q+'</div>'
|
||||||
|
+'<div class="boss-row"><input type="number" step="'+step+'" class="boss-input" id="fb-'+b.n+'-inp" placeholder="число"'+(solved?' value="'+dispAns+'" disabled':'')+'>'
|
||||||
|
+'<button class="boss-btn primary" id="fb-'+b.n+'-go"'+(solved?' disabled':'')+'>Атаковать</button>'
|
||||||
|
+'<button class="boss-btn" id="fb-'+b.n+'-hint">Подсказка</button></div>'
|
||||||
|
+'<div class="boss-hint-txt" id="fb-'+b.n+'-ht">'+b.hint+'</div>'
|
||||||
|
+'<div class="boss-fb'+(solved?' ok':'')+'" id="fb-'+b.n+'-fbk">'+(solved?'Победа! Босс повержен.':'')+'</div></div>';
|
||||||
|
}
|
||||||
|
function bindFinBoss(b){
|
||||||
|
var go=document.getElementById('fb-'+b.n+'-go'), hint=document.getElementById('fb-'+b.n+'-hint'),
|
||||||
|
inp=document.getElementById('fb-'+b.n+'-inp'), fbk=document.getElementById('fb-'+b.n+'-fbk'),
|
||||||
|
ht=document.getElementById('fb-'+b.n+'-ht'), card=document.getElementById('fb-'+b.n+'-card');
|
||||||
|
if(!go)return;
|
||||||
|
if(hint)hint.addEventListener('click',function(){ if(ht)ht.classList.toggle('show'); });
|
||||||
|
var state=loadFinBossState(); if(state[b.n])return;
|
||||||
|
go.addEventListener('click',function(){
|
||||||
|
var v=parseFloat((inp.value||'').replace(',','.'));
|
||||||
|
if(isNaN(v)){ fbk.className='boss-fb fail'; fbk.textContent='Введите число.'; return; }
|
||||||
|
var tol=(typeof b.tol==='number')?b.tol:1e-9;
|
||||||
|
if(Math.abs(v-b.ans)<=tol){
|
||||||
|
fbk.className='boss-fb ok'; fbk.textContent='Победа! +15 XP. Босс повержен.'; card.classList.add('solved'); go.disabled=true; inp.disabled=true;
|
||||||
|
var s=loadFinBossState(); if(!s[b.n]){ s[b.n]=true; saveFinBossState(s);
|
||||||
|
var xp=parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0; localStorage.setItem('chemistry8_xp',String(xp+15));
|
||||||
|
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(15,'chemistry8-fin-boss-'+b.n); }catch(e){}
|
||||||
|
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0)+' XP'; }
|
||||||
|
updateFinBossBar(s); maybeUnlockMaster(s);
|
||||||
|
}
|
||||||
|
} else { fbk.className='boss-fb fail'; fbk.textContent='Не то. Перепроверь решение и попробуй снова.'; }
|
||||||
|
});
|
||||||
|
inp.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); go.click(); } });
|
||||||
|
}
|
||||||
|
var FIN_BOSSES_RENDERED=false;
|
||||||
|
function renderFinBosses(){
|
||||||
|
if(FIN_BOSSES_RENDERED)return;
|
||||||
|
var cont=document.getElementById('fin-bosses-container'); if(!cont)return;
|
||||||
|
var state=loadFinBossState(), html='';
|
||||||
|
for(var i=0;i<FIN_BOSSES.length;i++) html+=buildFinBoss(FIN_BOSSES[i],state);
|
||||||
|
cont.innerHTML=html;
|
||||||
|
for(var j=0;j<FIN_BOSSES.length;j++) bindFinBoss(FIN_BOSSES[j]);
|
||||||
|
finRenderKatex(document.getElementById('course-final'));
|
||||||
|
updateFinBossBar(state);
|
||||||
|
if(localStorage.getItem(FIN_ACH_KEY)==='1'){ var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show'); }
|
||||||
|
FIN_BOSSES_RENDERED=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FINAL ACCORDION */
|
||||||
|
(function bindFinalAccordion(){
|
||||||
|
var head = document.getElementById('final-head');
|
||||||
|
var wrap = document.getElementById('course-final');
|
||||||
|
if (!head || !wrap) return;
|
||||||
|
function toggle(){
|
||||||
|
var willOpen = !wrap.classList.contains('open');
|
||||||
|
wrap.classList.toggle('open');
|
||||||
|
head.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
||||||
|
if (willOpen) { renderFinBosses(); finRenderKatex(wrap); }
|
||||||
|
}
|
||||||
|
head.addEventListener('click', toggle);
|
||||||
|
head.addEventListener('keydown', function(e){
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
function loadProgress() {
|
||||||
|
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
|
||||||
|
renderProgress([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.LS.api('/api/textbooks/chemistry-8/children')
|
||||||
|
.then(function(data) {
|
||||||
|
if (data && data.children) renderProgress(data.children);
|
||||||
|
else renderProgress([]);
|
||||||
|
})
|
||||||
|
.catch(function() { renderProgress([]); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', loadProgress);
|
||||||
|
} else {
|
||||||
|
loadProgress();
|
||||||
|
}
|
||||||
|
window.addEventListener('focus', loadProgress);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Химия 8 · Вводный раздел · «Количественные понятия в химии»</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/chem8-textbook.css">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||||
|
<script src="/js/api.js" defer></script>
|
||||||
|
<script src="/js/xp.js" defer></script>
|
||||||
|
<script src="/js/biochem-core.js" defer></script>
|
||||||
|
<script src="/js/chem8_svg.js" defer></script>
|
||||||
|
<script src="/js/chem8_glossary.js" defer></script>
|
||||||
|
<script src="/js/chem8_intro_widgets.js" defer></script>
|
||||||
|
<script src="/js/chem8_engine.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="hdr">
|
||||||
|
<div class="hdr-row">
|
||||||
|
<div>
|
||||||
|
<h1>Химия 8 · Вводный раздел</h1>
|
||||||
|
<div class="hdr-sub">Количественные понятия: атомы, формулы, моль, молярная масса и объём, расчёты по уравнениям</div>
|
||||||
|
</div>
|
||||||
|
<div class="hdr-side">
|
||||||
|
<a href="/textbook/chemistry-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К разделам</a>
|
||||||
|
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="col-main">
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<h2>Химия начинается со счёта</h2>
|
||||||
|
<p>Прежде чем изучать вещества и реакции, химик учится их «считать»: переходить от массы к числу частиц, от объёма газа — к количеству вещества, рассчитывать продукты реакции по уравнению. Эти количественные понятия — фундамент всего курса.</p>
|
||||||
|
<div class="hero-row">
|
||||||
|
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</button>
|
||||||
|
<div class="hero-progress">
|
||||||
|
<span class="hp-label">Прогресс раздела</span>
|
||||||
|
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
|
||||||
|
<span id="hero-hp-text" class="hp-text">0%</span>
|
||||||
|
</div>
|
||||||
|
<div id="hero-xp-badge" class="hero-xp-badge"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="psel">
|
||||||
|
<div class="psel-title">Параграфы раздела</div>
|
||||||
|
<div id="psel-grid" class="psel-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="sec-p1" class="sec"><div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Атомы. Химические элементы. Относительная атомная масса</h2></div><div id="p1-body"></div></section>
|
||||||
|
<section id="sec-p2" class="sec"><div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Молекулы. Простые и сложные вещества. Формулы. $M_r$</h2></div><div id="p2-body"></div></section>
|
||||||
|
<section id="sec-p3" class="sec"><div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Химическое количество вещества</h2></div><div id="p3-body"></div></section>
|
||||||
|
<section id="sec-p4" class="sec"><div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Моль. Постоянная Авогадро</h2></div><div id="p4-body"></div></section>
|
||||||
|
<section id="sec-p5" class="sec"><div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Молярная масса. Молярный объём газов</h2></div><div id="p5-body"></div></section>
|
||||||
|
<section id="sec-p6" class="sec"><div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Вычисление $n$ по массе и массы по $n$</h2></div><div id="p6-body"></div></section>
|
||||||
|
<section id="sec-p7" class="sec"><div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Вычисление количества и объёма газа</h2></div><div id="p7-body"></div></section>
|
||||||
|
<section id="sec-pr1" class="sec"><div class="sec-header"><span class="sec-num">ПР 1</span><h2 class="sec-h">Практическая работа: химическое количество вещества</h2></div><div id="pr1-body"></div></section>
|
||||||
|
<section id="sec-p8" class="sec"><div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Химические реакции</h2></div><div id="p8-body"></div></section>
|
||||||
|
<section id="sec-p9" class="sec"><div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Количественные расчёты по уравнениям реакций</h2></div><div id="p9-body"></div></section>
|
||||||
|
<section id="sec-final1" class="sec"><div class="sec-header"><span class="sec-num">★</span><h2 class="sec-h">Финал раздела</h2></div><div id="final1-body"></div></section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<aside class="col-side"><div id="sidebar-content"></div></aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">Интерактивный учебник «Химия — 8 класс» · Вводный раздел · «Количественные понятия в химии» · LearnSpace</footer>
|
||||||
|
<div id="ach-popup" class="ach-popup"><svg viewBox="0 0 24 24"><polygon points="12 2 22 20 2 20"/></svg><span id="ach-text">Достижение!</span></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
/* ── конфигурация раздела (читается движком chem8_engine.js) ── */
|
||||||
|
window.CHEM8_CFG = { slug:'chemistry-8-intro', themeKey:'chemistry8_theme', xpKey:'chemistry8_xp',
|
||||||
|
progKey:'chemistry8_intro_progress', achKey:'chemistry8_intro_ach' };
|
||||||
|
|
||||||
|
window.PARAS = [
|
||||||
|
{id:'p1',num:'§ 1',name:'Атомы. Атомная масса',sub:'элемент · $A_r$'},
|
||||||
|
{id:'p2',num:'§ 2',name:'Формулы. $M_r$',sub:'простые/сложные'},
|
||||||
|
{id:'p3',num:'§ 3',name:'Химическое количество',sub:'$n$, моль'},
|
||||||
|
{id:'p4',num:'§ 4',name:'Моль. Авогадро',sub:'$N=n\\,N_A$'},
|
||||||
|
{id:'p5',num:'§ 5',name:'Молярная масса и объём',sub:'$M$ · $V_m$'},
|
||||||
|
{id:'p6',num:'§ 6',name:'Треугольник n–m–M',sub:'$n=m/M$'},
|
||||||
|
{id:'p7',num:'§ 7',name:'Расчёты для газов',sub:'$V=n\\,V_m$'},
|
||||||
|
{id:'pr1',num:'ПР 1',name:'Практическая работа',sub:'кол-во вещества'},
|
||||||
|
{id:'p8',num:'§ 8',name:'Химические реакции',sub:'баланс уравнений'},
|
||||||
|
{id:'p9',num:'§ 9',name:'Расчёты по уравнениям',sub:'мольные отношения'},
|
||||||
|
{id:'final1',num:'★',name:'Финал раздела',sub:'босс · +ачивка',final:true}
|
||||||
|
];
|
||||||
|
|
||||||
|
window.ACH_LABELS = {
|
||||||
|
start:'Начало раздела!', p1_done:'§1 изучен!', p2_done:'§2 изучен!', p3_done:'§3 изучен!',
|
||||||
|
p4_done:'§4 изучен!', p5_done:'§5 изучен!', p6_done:'§6 изучен!', p7_done:'§7 изучен!',
|
||||||
|
p8_done:'§8 изучен!', p9_done:'§9 изучен!', pr1_done:'Практическая работа выполнена!',
|
||||||
|
final1_tasks:'Счёт в химии — раздел пройден!'
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SIDEBARS = {
|
||||||
|
p1:{title:'Шпаргалка §1',rows:[['Атом','химически неделимая частица'],['Элемент','вид атомов с одинаковым $Z$'],['$A_r$','масса атома / (1/12 масс. $^{12}$C)']]},
|
||||||
|
p2:{title:'Шпаргалка §2',rows:[['Простое','один элемент ($O_2$)'],['Сложное','разные ($H_2O$)'],['$M_r$','$=\\sum A_r$']]},
|
||||||
|
p3:{title:'Шпаргалка §3',rows:[['$n$','химическое количество'],['Единица','моль'],['Смысл','«порция» частиц']]},
|
||||||
|
p4:{title:'Шпаргалка §4',rows:[['$N_A$','$6{,}02\\cdot10^{23}$ 1/моль'],['$N=n\\,N_A$',''],['$n=N/N_A$','']]},
|
||||||
|
p5:{title:'Шпаргалка §5',rows:[['$M$','г/моль, $=M_r$'],['$V_m$','22,4 л/моль (н.у.)'],['Авогадро','равные $V$ → равные $n$']]},
|
||||||
|
p6:{title:'Шпаргалка §6',rows:[['$n=m/M$',''],['$m=n\\,M$',''],['$M=m/n$','']]},
|
||||||
|
p7:{title:'Шпаргалка §7',rows:[['$n=V/V_m$',''],['$V=n\\,V_m$',''],['Связка','$m$–$n$–$V$–$N$']]},
|
||||||
|
pr1:{title:'Практическая 1',rows:[['Цель','найти $n$ и $N$ по $m$'],['$n=m/M$',''],['$N=n\\,N_A$','']]},
|
||||||
|
p8:{title:'Шпаргалка §8',rows:[['Закон','сохранение массы'],['Баланс','атомы слева = справа'],['Типы','соед./разл./замещ./обмен']]},
|
||||||
|
p9:{title:'Шпаргалка §9',rows:[['Алгоритм','$m\\to n\\to n\\to m$'],['Отношение','по коэффициентам'],['Газ','через $V_m$']]},
|
||||||
|
final1:{title:'Финал раздела',rows:[['§§1–9','все расчёты'],['Награда','ачивка + XP']]}
|
||||||
|
};
|
||||||
|
window.TIPS = [
|
||||||
|
{sec:'p1',html:'$A_r$ показывает, во сколько раз атом тяжелее $1/12$ атома углерода-12. $A_r(\\text{H})=1$, $A_r(\\text{O})=16$.'},
|
||||||
|
{sec:'p2',html:'$M_r$ — сумма $A_r$ всех атомов формулы. $M_r(\\text{H}_2\\text{O})=2\\cdot1+16=18$.'},
|
||||||
|
{sec:'p3',html:'Моль — «порция» вещества. 1 моль любого вещества содержит одинаковое число частиц.'},
|
||||||
|
{sec:'p4',html:'$N=n\\cdot N_A$. В 1 моль — $6{,}02\\cdot10^{23}$ частиц.'},
|
||||||
|
{sec:'p5',html:'$M$ численно равна $M_r$. Для газа при н.у. $V_m=22{,}4$ л/моль.'},
|
||||||
|
{sec:'p6',html:'Закрой искомую величину в треугольнике — получишь формулу: $n=m/M$, $m=nM$, $M=m/n$.'},
|
||||||
|
{sec:'p7',html:'$n=V/V_m$ (газ, н.у.). Зная одно из $m,n,V,N$ — найдёшь остальные.'},
|
||||||
|
{sec:'pr1',html:'Взвесь вещество, найди $M$, вычисли $n=m/M$ и $N=n\\,N_A$.'},
|
||||||
|
{sec:'p8',html:'Атомы в реакции не исчезают: уравнивай коэффициентами, не меняя индексы в формулах.'},
|
||||||
|
{sec:'p9',html:'Сначала найди $n$ известного, по коэффициентам — $n$ искомого, затем переведи в $m$ или $V$.'},
|
||||||
|
{sec:'final1',html:'Собери всё: $M_r$, $n=m/M$, $V=n\\,V_m$, $N=n\\,N_A$, расчёты по уравнению.'}
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ── задачи (тренажёр) ── */
|
||||||
|
window.POOLS = {
|
||||||
|
p1:[
|
||||||
|
{q:'Что показывает относительная атомная масса $A_r$?',opts:['Массу атома в граммах','Во сколько раз масса атома больше $1/12$ массы атома $^{12}$C','Число протонов в ядре','Число электронов'],a:1,ex:'$A_r$ — безразмерная величина сравнения с $1/12$ массы атома углерода-12.'},
|
||||||
|
{q:'$A_r(\\text{S})=32$, $A_r(\\text{O})=16$. Во сколько раз атом серы тяжелее атома кислорода?',hint:'$32/16$',unit:'раза',a:2,ex:'$32/16=2$.'},
|
||||||
|
{q:'Сколько протонов в ядре атома кальция (порядковый номер $Z=20$)?',hint:'число протонов $=Z$',unit:'',a:20,ex:'Заряд ядра равен порядковому номеру: 20 протонов.'},
|
||||||
|
{q:'Химический элемент — это…',opts:['Любая мельчайшая частица','Вид атомов с одинаковым зарядом ядра','Молекула вещества','Смесь атомов'],a:1,ex:'Элемент определяется зарядом ядра (числом протонов).'}
|
||||||
|
],
|
||||||
|
p2:[
|
||||||
|
{q:'Чему равна $M_r(\\text{H}_2\\text{O})$?',hint:'$2\\cdot1+16$',unit:'',a:18,ex:'$M_r=2\\cdot1+16=18$.'},
|
||||||
|
{q:'Чему равна $M_r(\\text{CO}_2)$?',hint:'$12+2\\cdot16$',unit:'',a:44,ex:'$12+32=44$.'},
|
||||||
|
{q:'$\\text{O}_2$ — это…',opts:['Сложное вещество','Простое вещество','Смесь','Раствор'],a:1,ex:'Образовано атомами одного элемента — простое вещество.'},
|
||||||
|
{q:'Чему равна $M_r(\\text{H}_2\\text{SO}_4)$?',hint:'$2+32+4\\cdot16$',unit:'',a:98,ex:'$2+32+64=98$.'},
|
||||||
|
{q:'Чему равна $M_r(\\text{CaCO}_3)$?',hint:'$40+12+3\\cdot16$',unit:'',a:100,ex:'$40+12+48=100$.'}
|
||||||
|
],
|
||||||
|
p3:[
|
||||||
|
{q:'Единица химического количества вещества — это…',opts:['Грамм','Литр','Моль','Паскаль'],a:2,ex:'Химическое количество измеряют в молях.'},
|
||||||
|
{q:'1 моль любого вещества содержит…',opts:['1 грамм вещества','Одинаковое число частиц','1 литр газа','Разное число частиц'],a:1,ex:'Это и есть смысл моля — фиксированное число частиц.'}
|
||||||
|
],
|
||||||
|
p4:[
|
||||||
|
{q:'Постоянная Авогадро $N_A$ примерно равна…',opts:['$3{,}01\\cdot10^{23}$','$6{,}02\\cdot10^{23}$','$9{,}8$','$22{,}4$'],a:1,ex:'$N_A=6{,}02\\cdot10^{23}$ частиц/моль.'},
|
||||||
|
{q:'Сколько молекул в $2$ моль воды? Ответ — коэффициент при $\\cdot10^{23}$.',hint:'$N=n\\,N_A=2\\cdot6{,}02$',unit:'$\\cdot10^{23}$',a:12.04,ex:'$2\\cdot6{,}02=12{,}04$ (·10²³).'},
|
||||||
|
{q:'Чему равно $n$, если число молекул $N=3{,}01\\cdot10^{23}$?',hint:'$n=N/N_A$',unit:'моль',a:0.5,ex:'$3{,}01/6{,}02=0{,}5$ моль.'}
|
||||||
|
],
|
||||||
|
p5:[
|
||||||
|
{q:'Чему равна молярная масса $M(\\text{CO}_2)$?',hint:'численно $=M_r$',unit:'г/моль',a:44,ex:'$M=M_r=44$ г/моль.'},
|
||||||
|
{q:'Какой объём занимают $2$ моль любого газа при н.у.?',hint:'$V=n\\,V_m$',unit:'л',a:44.8,ex:'$2\\cdot22{,}4=44{,}8$ л.'},
|
||||||
|
{q:'Чему равно $n$ газа, если его объём при н.у. $V=11{,}2$ л?',hint:'$n=V/V_m$',unit:'моль',a:0.5,ex:'$11{,}2/22{,}4=0{,}5$ моль.'}
|
||||||
|
],
|
||||||
|
p6:[
|
||||||
|
{q:'Найди $n$ воды, если $m=36$ г, $M=18$ г/моль.',hint:'$n=m/M$',unit:'моль',a:2,ex:'$36/18=2$ моль.'},
|
||||||
|
{q:'Найди массу $0{,}5$ моль $\\text{NaOH}$ ($M=40$ г/моль).',hint:'$m=n\\,M$',unit:'г',a:20,ex:'$0{,}5\\cdot40=20$ г.'},
|
||||||
|
{q:'Найди $M$ вещества, если $m=44$ г при $n=1$ моль.',hint:'$M=m/n$',unit:'г/моль',a:44,ex:'$44/1=44$ г/моль.'},
|
||||||
|
{q:'Найди $n$ карбоната кальция, если $m=50$ г, $M=100$ г/моль.',hint:'$n=m/M$',unit:'моль',a:0.5,ex:'$50/100=0{,}5$ моль.'}
|
||||||
|
],
|
||||||
|
p7:[
|
||||||
|
{q:'Какой объём (н.у.) занимают $3$ моль кислорода?',hint:'$V=n\\,V_m$',unit:'л',a:67.2,ex:'$3\\cdot22{,}4=67{,}2$ л.'},
|
||||||
|
{q:'Найди $n$ газа, если $V=44{,}8$ л при н.у.',hint:'$n=V/V_m$',unit:'моль',a:2,ex:'$44{,}8/22{,}4=2$ моль.'},
|
||||||
|
{q:'Кислорода взяли $8$ г ($M=32$). Какой объём (н.у.) он займёт?',hint:'$n=m/M$, затем $V=n\\,V_m$',unit:'л',a:5.6,ex:'$n=8/32=0{,}25$; $V=0{,}25\\cdot22{,}4=5{,}6$ л.'}
|
||||||
|
],
|
||||||
|
p8:[
|
||||||
|
{q:'Закон сохранения массы означает, что…',opts:['Масса всегда растёт','Масса реагентов равна массе продуктов','Атомы исчезают','Масса уменьшается'],a:1,ex:'Атомы не исчезают и не появляются — масса сохраняется.'},
|
||||||
|
{q:'Реакция $\\text{CaCO}_3\\to\\text{CaO}+\\text{CO}_2$ относится к типу…',opts:['Соединения','Разложения','Замещения','Обмена'],a:1,ex:'Из одного вещества — несколько: разложение.'},
|
||||||
|
{q:'Реакция вида $A+B\\to AB$ — это реакция…',opts:['Разложения','Соединения','Обмена','Замещения'],a:1,ex:'Из нескольких веществ — одно: соединение.'},
|
||||||
|
{q:'В уравнении $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$ коэффициент перед $\\text{H}_2\\text{O}$ равен…',hint:'смотри на воду',unit:'',a:2,ex:'Коэффициент 2.'}
|
||||||
|
],
|
||||||
|
p9:[
|
||||||
|
{q:'$2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$. Дано $m(\\text{H}_2)=4$ г ($M=2$). Найди $m(\\text{H}_2\\text{O})$ ($M=18$).',hint:'$n(\\text{H}_2)=4/2=2$; $n(\\text{H}_2\\text{O})=2$; $m=2\\cdot18$',unit:'г',a:36,ex:'$m=2\\cdot18=36$ г.'},
|
||||||
|
{q:'$\\text{CaCO}_3=\\text{CaO}+\\text{CO}_2$. Дано $m(\\text{CaCO}_3)=100$ г ($M=100$). Найди $V(\\text{CO}_2)$ при н.у.',hint:'$n=1$; $V=n\\,V_m$',unit:'л',a:22.4,ex:'$n=1$ моль → $V=22{,}4$ л.'},
|
||||||
|
{q:'$\\text{Zn}+2\\text{HCl}=\\text{ZnCl}_2+\\text{H}_2$. Дано $n(\\text{Zn})=0{,}5$ моль. Найди $V(\\text{H}_2)$ при н.у.',hint:'$n(\\text{H}_2)=0{,}5$; $V=n\\,V_m$',unit:'л',a:11.2,ex:'$0{,}5\\cdot22{,}4=11{,}2$ л.'}
|
||||||
|
],
|
||||||
|
final1:[
|
||||||
|
{q:'$M_r(\\text{H}_2\\text{SO}_4)=?$',hint:'$2+32+64$',unit:'',a:98,ex:'$98$.'},
|
||||||
|
{q:'Сколько моль в $80$ г $\\text{NaOH}$ ($M=40$)?',hint:'$n=m/M$',unit:'моль',a:2,ex:'$80/40=2$ моль.'},
|
||||||
|
{q:'Объём (н.у.) $3$ моль $\\text{O}_2$?',hint:'$V=n\\,V_m$',unit:'л',a:67.2,ex:'$67{,}2$ л.'},
|
||||||
|
{q:'Число молекул в $2$ моль воды (коэф. при $\\cdot10^{23}$)?',hint:'$2\\cdot6{,}02$',unit:'$\\cdot10^{23}$',a:12.04,ex:'$12{,}04$.'},
|
||||||
|
{q:'$M_r(\\text{Fe}_2\\text{O}_3)=?$',hint:'$2\\cdot56+3\\cdot16$',unit:'',a:160,ex:'$112+48=160$.'},
|
||||||
|
{q:'$\\text{S}+\\text{O}_2=\\text{SO}_2$. Дано $n(\\text{S})=2$ моль. Найди $V(\\text{SO}_2)$ при н.у.',hint:'$n(\\text{SO}_2)=2$; $V=n\\,V_m$',unit:'л',a:44.8,ex:'$2\\cdot22{,}4=44{,}8$ л.'},
|
||||||
|
{q:'Какой объём (л, н.у.) занимают 0,5 моль CO₂?',hint:'V=n·22,4',unit:'л',a:11.2,tol:0.05,step:'0.1',ex:'0,5·22,4=11,2 л.'},
|
||||||
|
{q:'Mr(Na₂CO₃)?',hint:'2·23+12+3·16',unit:'',a:106,ex:'46+12+48=106.'} /*U5-extra*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── вспомогательные конструкторы контента ── */
|
||||||
|
function rememberBox(items){
|
||||||
|
return '<div class="remember-box"><div class="remember-box-title">'
|
||||||
|
+'<svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Запомни!</div><ul>'
|
||||||
|
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ul></div>';
|
||||||
|
}
|
||||||
|
function qList(items){
|
||||||
|
return '<div class="section-title">Вопросы и задания</div><ol class="q-list">'
|
||||||
|
+items.map(function(t){return '<li>'+t+'</li>';}).join('')+'</ol>';
|
||||||
|
}
|
||||||
|
function wgt(title, inner){
|
||||||
|
return '<div class="wgt"><div class="wgt-h"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg> '+title+'</div>'+inner+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── BUILDERS параграфов ── */
|
||||||
|
window.BUILDERS = {
|
||||||
|
p1:build_p1,p2:build_p2,p3:build_p3,p4:build_p4,p5:build_p5,p6:build_p6,p7:build_p7,
|
||||||
|
pr1:build_pr1,p8:build_p8,p9:build_p9,final1:build_final1
|
||||||
|
};
|
||||||
|
|
||||||
|
function build_p1(){
|
||||||
|
document.getElementById('p1-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-1"><div class="ph-label">§ 1 · Химия 8 кл</div><h2>Атомы. Химические элементы. Относительная атомная масса</h2>'
|
||||||
|
+'<div class="ph-formula">$A_r(\\text{O}) = 16$</div>'
|
||||||
|
+'<div class="ph-desc">Из чего состоят вещества и как сравнивают массы атомов, которые невозможно взвесить по отдельности.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">атом</span><span class="ph-tag">элемент</span><span class="ph-tag">$A_r$</span></div></div>'
|
||||||
|
+makeCard('theory','Атом и химический элемент','§1','<p><b>Атом</b> — мельчайшая химически неделимая частица вещества. <b>Химический элемент</b> — вид атомов с одинаковым зарядом ядра. Каждый элемент имеет символ ($\\text{H}$, $\\text{O}$, $\\text{Fe}$) и порядковый номер $Z$, равный числу протонов в ядре.</p>'
|
||||||
|
+'<div class="def-box"><b>Относительная атомная масса</b> $A_r$ показывает, во сколько раз масса атома больше $\\tfrac{1}{12}$ массы атома углерода-12. Это безразмерная величина: $A_r(\\text{H})=1$, $A_r(\\text{O})=16$, $A_r(\\text{Fe})=56$.</div>')
|
||||||
|
+makeCard('example','Сравнение масс атомов',null,'<p>Во сколько раз атом кислорода тяжелее атома водорода?</p><div class="exa-step">$\\dfrac{A_r(\\text{O})}{A_r(\\text{H})}=\\dfrac{16}{1}=16$ раз.</div>')
|
||||||
|
+wgt('Карта элементов: клик → $Z$, название, $A_r$','<div class="el-grid" id="p1-el"></div><div class="el-info" id="p1-elinfo">Выберите элемент, чтобы увидеть его характеристики.</div>')
|
||||||
|
+rememberBox(['Атом нейтрален: число протонов = числу электронов.','$A_r$ — безразмерная, берётся из периодической таблицы.','Заряд ядра (число протонов) = порядковый номер $Z$.'])
|
||||||
|
+qList(['Чем отличается атом от химического элемента?','Что показывает $A_r$ и почему она безразмерна?','Атом магния имеет $A_r=24$. Во сколько раз он тяжелее атома углерода?'])
|
||||||
|
+secNav(null,'p2')+readButton('p1');
|
||||||
|
wireReadBtn('p1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p2(){
|
||||||
|
document.getElementById('p2-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-2"><div class="ph-label">§ 2 · Химия 8 кл</div><h2>Молекулы. Простые и сложные вещества. Формулы. $M_r$</h2>'
|
||||||
|
+'<div class="ph-formula">$M_r=\\sum A_r$</div>'
|
||||||
|
+'<div class="ph-desc">Как записывают состав вещества формулой и как по ней рассчитывают относительную молекулярную массу.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">формула</span><span class="ph-tag">индекс</span><span class="ph-tag">$M_r$</span></div></div>'
|
||||||
|
+makeCard('theory','Вещества и формулы','§2','<p><b>Простое вещество</b> образовано атомами одного элемента ($\\text{O}_2$, $\\text{Fe}$), <b>сложное</b> — разных ($\\text{H}_2\\text{O}$, $\\text{CaCO}_3$). <b>Химическая формула</b> показывает качественный (какие элементы) и количественный (сколько атомов) состав. Нижний индекс — число атомов элемента.</p>'
|
||||||
|
+'<div class="def-box"><b>Относительная молекулярная масса</b> $M_r$ равна сумме относительных атомных масс всех атомов формулы: $M_r(\\text{H}_2\\text{O})=2\\cdot1+16=18$.</div>')
|
||||||
|
+makeCard('example','Расчёт $M_r$',null,'<p>$M_r(\\text{H}_2\\text{SO}_4)$:</p><div class="exa-step">$2\\cdot A_r(\\text{H})+A_r(\\text{S})+4\\cdot A_r(\\text{O}) = 2\\cdot1+32+4\\cdot16 = 98$.</div>')
|
||||||
|
+wgt('Калькулятор $M_r$ по формуле','<div class="fld"><label>Формула</label><input type="text" id="p2-mr-in" value="CaCO3" style="width:160px;font-family:var(--mono)"><button class="btn primary" id="p2-mr-go">Вычислить</button></div>'
|
||||||
|
+'<div class="fld" style="gap:6px"><button class="btn p2-ex" data-f="H2O">H₂O</button><button class="btn p2-ex" data-f="H2SO4">H₂SO₄</button><button class="btn p2-ex" data-f="Ca(OH)2">Ca(OH)₂</button><button class="btn p2-ex" data-f="Al2(SO4)3">Al₂(SO₄)₃</button></div>'
|
||||||
|
+'<div class="out" id="p2-mr-out">Введите формулу и нажмите «Вычислить».</div>')
|
||||||
|
+rememberBox(['Индекс относится только к стоящему перед ним атому/группе.','Скобки: индекс умножает всё внутри — $\\text{Ca(OH)}_2$ = 1 Ca, 2 O, 2 H.','$M_r$ численно совпадает с молярной массой $M$ (§5).'])
|
||||||
|
+qList(['Чем простое вещество отличается от сложного?','Вычисли $M_r(\\text{Na}_2\\text{CO}_3)$.','Сколько всего атомов в формуле $\\text{Al}_2(\\text{SO}_4)_3$?'])
|
||||||
|
+secNav('p1','p3')+readButton('p2');
|
||||||
|
wireReadBtn('p2');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p3(){
|
||||||
|
document.getElementById('p3-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-3"><div class="ph-label">§ 3 · Химия 8 кл</div><h2>Химическое количество вещества</h2>'
|
||||||
|
+'<div class="ph-formula">$n$, моль</div>'
|
||||||
|
+'<div class="ph-desc">Почему химики считают вещество «порциями», а не отдельными атомами.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$n$</span><span class="ph-tag">моль</span><span class="ph-tag">порция</span></div></div>'
|
||||||
|
+makeCard('theory','Зачем нужна «порция»','§3','<p>Атомы и молекулы невозможно считать поштучно — их слишком много даже в крупинке вещества. Поэтому ввели специальную «порцию» — <b>химическое количество вещества</b> $n$, измеряемое в <b>молях</b>.</p>'
|
||||||
|
+'<div class="def-box">Одна и та же порция — $1$ моль — любого вещества содержит <b>одинаковое число частиц</b>. Химическое количество $n$ связывает массу $m$, число частиц $N$ и объём газа $V$ — это «мост» между атомами и граммами на весах.</div>')
|
||||||
|
+wgt('Порция вещества: $n \\Rightarrow N$ и $m$','<div class="fld"><label>Вещество</label><select id="p3-sub"><option value="H2O">вода H₂O (M=18)</option><option value="O2">кислород O₂ (M=32)</option><option value="CO2">углекислый газ CO₂ (M=44)</option><option value="NaCl">соль NaCl (M=58,5)</option></select><label>n, моль</label><input type="range" id="p3-n" min="0.1" max="5" step="0.1" value="1"><span class="bd" id="p3-nv">1,0</span></div><div class="out" id="p3-out"></div>')
|
||||||
|
+rememberBox(['$n$ измеряют в молях.','1 моль ≠ 1 грамм: масса 1 моль зависит от вещества (§5).','Чем больше $n$, тем больше и масса, и число частиц.'])
|
||||||
|
+qList(['Почему вещество удобнее измерять в молях, а не поштучно?','Что общего у 1 моль воды и 1 моль железа?'])
|
||||||
|
+secNav('p2','p4')+readButton('p3');
|
||||||
|
wireReadBtn('p3');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p4(){
|
||||||
|
document.getElementById('p4-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-4"><div class="ph-label">§ 4 · Химия 8 кл</div><h2>Моль. Постоянная Авогадро</h2>'
|
||||||
|
+'<div class="ph-formula">$N = n\\cdot N_A$</div>'
|
||||||
|
+'<div class="ph-desc">Сколько именно частиц содержит одна порция-моль и как считать их число.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$N_A$</span><span class="ph-tag">$6{,}02\\cdot10^{23}$</span></div></div>'
|
||||||
|
+makeCard('theory','Постоянная Авогадро','§4','<div class="def-box"><b>1 моль</b> — химическое количество вещества, содержащее столько же частиц, сколько атомов в $12$ г углерода-12, а именно $N_A = 6{,}02\\cdot10^{23}$ частиц/моль — <b>постоянная Авогадро</b>.</div>'
|
||||||
|
+'<p>Число частиц вычисляют по формуле $N = n\\cdot N_A$, а количество вещества — $n = \\dfrac{N}{N_A}$.</p>')
|
||||||
|
+makeCard('example','Сколько частиц?',null,'<p>Сколько молекул в $0{,}5$ моль воды?</p><div class="exa-step">$N=n\\,N_A=0{,}5\\cdot6{,}02\\cdot10^{23}=3{,}01\\cdot10^{23}$ молекул.</div>')
|
||||||
|
+wgt('Счётчик частиц $N = n\\cdot N_A$','<div class="fld"><label>n, моль</label><input type="range" id="p4-n" min="0.25" max="10" step="0.25" value="2"><span class="bd" id="p4-nv">2,0</span></div><div class="out" id="p4-out"></div>')
|
||||||
|
+rememberBox(['$N_A=6{,}02\\cdot10^{23}$ — огромное число: столько частиц в одной порции-моль.','$N=n\\,N_A$ — частиц; $n=N/N_A$ — молей.'])
|
||||||
|
+qList(['Сколько атомов в 2 моль железа?','Сколько моль составляют $1{,}204\\cdot10^{24}$ молекул?'])
|
||||||
|
+secNav('p3','p5')+readButton('p4');
|
||||||
|
wireReadBtn('p4');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p5(){
|
||||||
|
document.getElementById('p5-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-5"><div class="ph-label">§ 5 · Химия 8 кл</div><h2>Молярная масса. Молярный объём газов</h2>'
|
||||||
|
+'<div class="ph-formula">$V_m=22{,}4$ л/моль</div>'
|
||||||
|
+'<div class="ph-desc">Масса и объём одной порции-моль: для любого вещества и для любого газа.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$M$</span><span class="ph-tag">$V_m$</span><span class="ph-tag">н.у.</span></div></div>'
|
||||||
|
+makeCard('theory','M и Vm','§5','<div class="def-box"><b>Молярная масса</b> $M$ — масса $1$ моль вещества, г/моль. Численно $M$ равна $M_r$: $M(\\text{H}_2\\text{O})=18$ г/моль.</div>'
|
||||||
|
+'<div class="def-box"><b>Молярный объём</b> $V_m$ — объём $1$ моль газа. При нормальных условиях (н.у., $0\\,^\\circ$C и $101{,}3$ кПа) $V_m = 22{,}4$ л/моль для <b>любого</b> газа (следствие закона Авогадро).</div>')
|
||||||
|
+wgt('M по формуле и объём 1 моль газа','<div class="fld"><label>Формула газа</label><input type="text" id="p5-in" value="CO2" style="width:140px;font-family:var(--mono)"><button class="btn primary" id="p5-go">Найти M</button></div><div class="out" id="p5-out">M(CO₂) и объём при н.у. появятся здесь.</div>')
|
||||||
|
+'<div class="insight-box"><div class="insight-title"><svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg> Закон Авогадро</div><p>В равных объёмах разных газов при одинаковых условиях — <b>равное число молекул</b>. Поэтому 1 моль любого газа занимает одинаковый объём 22,4 л.</p></div>'
|
||||||
|
+rememberBox(['$M$ (г/моль) численно равна $M_r$.','$V_m=22{,}4$ л/моль — только для газов и только при н.у.','У жидкостей и твёрдых веществ молярного объёма 22,4 л нет.'])
|
||||||
|
+qList(['Чему равна $M(\\text{NaOH})$?','Какой объём при н.у. займут 0,5 моль любого газа?'])
|
||||||
|
+secNav('p4','p6')+readButton('p5');
|
||||||
|
wireReadBtn('p5');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p6(){
|
||||||
|
document.getElementById('p6-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-6"><div class="ph-label">§ 6 · звёздный виджет</div><h2>Вычисление $n$ по массе и массы по $n$</h2>'
|
||||||
|
+'<div class="ph-formula">$n = \\dfrac{m}{M}$</div>'
|
||||||
|
+'<div class="ph-desc">Три связанные величины и удобный «треугольник» для запоминания формул.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$n=m/M$</span><span class="ph-tag">$m=nM$</span><span class="ph-tag">$M=m/n$</span></div></div>'
|
||||||
|
+makeCard('rule','Треугольник n–m–M','§6','<p>Все три величины связаны формулой $m = n\\cdot M$. Закрой искомую в треугольнике — получишь нужную формулу: $n=\\dfrac{m}{M}$, $m=n\\cdot M$, $M=\\dfrac{m}{n}$.</p>')
|
||||||
|
+makeCard('example','Разбор',null,'<p>Дано: $m=36$ г воды, $M=18$ г/моль. Найти $n$.</p><div class="exa-step">$n = \\dfrac{m}{M} = \\dfrac{36}{18} = 2$ моль.</div>')
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Интерактивный треугольник n–m–M</div><div class="flag-help">Введи любые два значения — третье вычислится. Можно подставить $M$ вещества.</div>'
|
||||||
|
+'<div class="fld"><label>Подставить M вещества</label><select id="p6-sub"><option value="">— вручную —</option><option value="H2O">H₂O · 18</option><option value="CO2">CO₂ · 44</option><option value="NaOH">NaOH · 40</option><option value="CaCO3">CaCO₃ · 100</option><option value="H2SO4">H₂SO₄ · 98</option></select></div><div id="p6-mount"></div></div>'
|
||||||
|
+rememberBox(['$m=n\\cdot M$ — основная формула, остальные выводятся из неё.','Единицы: $n$ — моль, $m$ — г, $M$ — г/моль.','Проверяй размерность: г / (г/моль) = моль.'])
|
||||||
|
+qList(['Найди массу 2 моль $\\text{CO}_2$.','Сколько моль в 40 г $\\text{NaOH}$?'])
|
||||||
|
+secNav('p5','p7')+readButton('p6');
|
||||||
|
wireReadBtn('p6');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p7(){
|
||||||
|
document.getElementById('p7-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-7"><div class="ph-label">§ 7 · Химия 8 кл</div><h2>Вычисление количества и объёма газа</h2>'
|
||||||
|
+'<div class="ph-formula">$n = \\dfrac{V}{V_m}$</div>'
|
||||||
|
+'<div class="ph-desc">Объёмы газов — через молярный объём. Единая связка четырёх величин.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$V=n\\,V_m$</span><span class="ph-tag">m–n–V–N</span></div></div>'
|
||||||
|
+makeCard('rule','Связка m – n – V – N','§7','<p>Для газа при н.у.: $n=\\dfrac{V}{V_m}$ и $V=n\\cdot V_m$, где $V_m=22{,}4$ л/моль. Вместе с $n=\\dfrac{m}{M}$ и $N=n\\cdot N_A$ это единая система: зная любую величину, найдёшь все остальные через $n$.</p>'
|
||||||
|
+'<div class="def-box">$m \\xrightarrow{\\;:M\\;} n \\xrightarrow{\\;\\cdot V_m\\;} V$, и $n \\xrightarrow{\\;\\cdot N_A\\;} N$. Центр всей системы — количество вещества $n$.</div>')
|
||||||
|
+makeCard('example','Через массу к объёму',null,'<p>$8$ г кислорода ($M=32$). Какой объём он займёт при н.у.?</p><div class="exa-step">$n=\\dfrac{8}{32}=0{,}25$ моль → $V=0{,}25\\cdot22{,}4=5{,}6$ л.</div>')
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Универсальный калькулятор газа</div><div class="flag-help">Выбери газ, укажи известную величину — получишь $n$, $m$, $V$ и $N$.</div>'
|
||||||
|
+'<div class="fld"><label>Газ</label><select id="p7-sub"><option value="O2">O₂ · M=32</option><option value="CO2">CO₂ · M=44</option><option value="H2">H₂ · M=2</option><option value="N2">N₂ · M=28</option></select></div>'
|
||||||
|
+'<div class="fld"><label>Известно</label><select id="p7-key"><option value="n">n, моль</option><option value="m">m, г</option><option value="V">V, л (н.у.)</option><option value="N">N, ·10²³</option></select><input type="text" id="p7-val" value="1" style="width:110px;font-family:var(--mono)"><button class="btn primary" id="p7-go">Рассчитать</button></div><div class="out" id="p7-out"></div></div>'
|
||||||
|
+rememberBox(['$V_m=22{,}4$ л/моль — только при н.у.','Всегда сначала находи $n$, потом переходи к нужной величине.'])
|
||||||
|
+qList(['Какой объём займут 3 моль водорода при н.у.?','Сколько моль газа в 44,8 л при н.у.?'])
|
||||||
|
+secNav('p6','pr1')+readButton('p7');
|
||||||
|
wireReadBtn('p7');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_pr1(){
|
||||||
|
document.getElementById('pr1-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-pr"><div class="ph-label">Практическая работа 1</div><h2>Химическое количество вещества</h2>'
|
||||||
|
+'<div class="ph-formula">$n=\\dfrac{m}{M}$, $N=n\\,N_A$</div>'
|
||||||
|
+'<div class="ph-desc">Связать измеримую массу с числом частиц через химическое количество.</div></div>'
|
||||||
|
+makeCard('lab','Порядок работы',null,'<ol><li>Взвесь на весах образцы веществ (например, $\\text{NaCl}$, $\\text{CuSO}_4$).</li><li>Запиши массу $m$ и определи молярную массу $M$ по формуле.</li><li>Вычисли химическое количество $n=\\dfrac{m}{M}$.</li><li>Найди число частиц $N=n\\cdot N_A$.</li><li>Оформи вывод: какому числу частиц соответствует взятая масса.</li></ol>'
|
||||||
|
+'<div class="note-safe"><svg viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg> Работай аккуратно с реактивами и весами; не пробуй вещества на вкус.</div>')
|
||||||
|
+wgt('Тренажёр расчёта (как в работе)','<div class="fld"><label>Подставить M</label><select id="pr1-sub"><option value="">— вручную —</option><option value="NaCl">NaCl · 58,5</option><option value="CuSO4">CuSO₄ · 160</option><option value="H2O">H₂O · 18</option></select></div><div id="pr1-mount"></div>')
|
||||||
|
+secNav('p7','p8')+readButton('pr1');
|
||||||
|
wireReadBtn('pr1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p8(){
|
||||||
|
document.getElementById('p8-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-8"><div class="ph-label">§ 8 · звёздный виджет</div><h2>Химические реакции</h2>'
|
||||||
|
+'<div class="ph-formula">закон сохранения массы</div>'
|
||||||
|
+'<div class="ph-desc">Что происходит с атомами в реакции и как уравнивают химические уравнения.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">баланс</span><span class="ph-tag">коэффициенты</span><span class="ph-tag">типы</span></div></div>'
|
||||||
|
+makeCard('theory','Уравнение реакции','§8','<p>В химической реакции одни вещества превращаются в другие, но <b>атомы не исчезают и не появляются</b> — это <b>закон сохранения массы</b> (М. В. Ломоносов, А. Лавуазье). Поэтому уравнение реакции <b>уравнивают коэффициентами</b>: число атомов каждого элемента слева и справа должно быть равным. Менять индексы внутри формул нельзя!</p>')
|
||||||
|
+makeCard('example','Типы реакций',null,'<ul><li><b>Соединения:</b> $A+B\\to AB$ — из нескольких веществ одно.</li><li><b>Разложения:</b> $AB\\to A+B$ — из одного несколько.</li><li><b>Замещения:</b> $A+BC\\to AC+B$.</li><li><b>Обмена:</b> $AB+CD\\to AD+CB$.</li></ul>')
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Балансировщик: расставь коэффициенты</div><div class="flag-help">Подбери коэффициенты так, чтобы число атомов каждого элемента совпало слева и справа.</div>'
|
||||||
|
+'<div class="fld" style="gap:6px"><label>Реакция</label><select id="p8-pick"><option value="H2 + O2 -> H2O|2,1,2">H₂ + O₂ → H₂O</option><option value="Fe + O2 -> Fe2O3|4,3,2">Fe + O₂ → Fe₂O₃</option><option value="Al + HCl -> AlCl3 + H2|2,6,2,3">Al + HCl → AlCl₃ + H₂</option><option value="CH4 + O2 -> CO2 + H2O|1,2,1,2">CH₄ + O₂ → CO₂ + H₂O</option></select></div><div id="p8-mount"></div></div>'
|
||||||
|
+rememberBox(['Уравнивают только коэффициентами, индексы не трогают.','Коэффициент относится ко всей формуле сразу.','Сумма масс реагентов = сумме масс продуктов.'])
|
||||||
|
+qList(['Сформулируй закон сохранения массы.','К какому типу относится реакция $\\text{Zn}+\\text{CuSO}_4\\to\\text{ZnSO}_4+\\text{Cu}$?'])
|
||||||
|
+secNav('pr1','p9')+readButton('p8');
|
||||||
|
wireReadBtn('p8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_p9(){
|
||||||
|
document.getElementById('p9-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-9"><div class="ph-label">§ 9 · звёздный виджет</div><h2>Количественные расчёты по уравнениям реакций</h2>'
|
||||||
|
+'<div class="ph-formula">по мольным отношениям</div>'
|
||||||
|
+'<div class="ph-desc">Главный приём химии: по массе одного вещества рассчитать массу или объём другого.</div>'
|
||||||
|
+'<div class="ph-tags"><span class="ph-tag">$n$</span><span class="ph-tag">коэффициенты</span><span class="ph-tag">$m$, $V$</span></div></div>'
|
||||||
|
+makeCard('rule','Алгоритм расчёта','§9','<ol><li>Записать и уравнять уравнение реакции.</li><li>Найти $n$ известного вещества: $n=\\dfrac{m}{M}$ (или $\\dfrac{V}{V_m}$).</li><li>По коэффициентам найти $n$ искомого (мольное отношение).</li><li>Перейти к массе или объёму: $m=n\\cdot M$ ($V=n\\cdot V_m$).</li></ol>')
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Пошаговый решатель по уравнению</div><div class="flag-help">Выбери задачу и раскрывай решение по шагам.</div>'
|
||||||
|
+'<div class="fld"><label>Задача</label><select id="p9-pick"></select></div><div class="out" id="p9-out"></div>'
|
||||||
|
+'<div class="fld"><button class="btn" id="p9-step">Следующий шаг ▸</button><button class="btn" id="p9-all">Показать всё решение</button></div></div>'
|
||||||
|
+rememberBox(['Мольное отношение берут из коэффициентов уравнения.','Уравнение обязательно уравнивают до расчёта.','Для газов используют $V=n\\,V_m$ вместо $m=n\\,M$.'])
|
||||||
|
+qList(['$\\text{CaCO}_3=\\text{CaO}+\\text{CO}_2$. Сколько $\\text{CO}_2$ (л, н.у.) даст 200 г $\\text{CaCO}_3$?','$2\\text{Mg}+\\text{O}_2=2\\text{MgO}$. Сколько MgO даст 2 моль Mg?'])
|
||||||
|
+secNav('p8','final1')+readButton('p9');
|
||||||
|
wireReadBtn('p9');
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_final1(){
|
||||||
|
document.getElementById('final1-body').innerHTML =
|
||||||
|
'<div class="para-hero ph-final"><div class="ph-label">Финал раздела</div><h2>Босс: количественные понятия</h2>'
|
||||||
|
+'<div class="ph-formula">$M_r$ · $n=m/M$ · $V=n\\,V_m$ · $N=n\\,N_A$</div>'
|
||||||
|
+'<div class="ph-desc">Шесть интегрированных задач на всё, что изучено. Реши все — получи ачивку «Счёт в химии».</div></div>'
|
||||||
|
+makeCard('rule','Шпаргалка раздела',null,'<div class="formula-grid">'
|
||||||
|
+'<div class="fcard"><h3>Масса ↔ количество</h3><div class="main-f">n = m / M</div></div>'
|
||||||
|
+'<div class="fcard"><h3>Объём газа</h3><div class="main-f">V = n · 22,4</div></div>'
|
||||||
|
+'<div class="fcard"><h3>Число частиц</h3><div class="main-f">N = n · 6,02·10²³</div></div>'
|
||||||
|
+'<div class="fcard highlight"><h3>Связка</h3><div class="main-f">m → n → V, N</div></div></div>')
|
||||||
|
+'<p style="margin:10px 0;color:var(--muted);font-size:.9rem">Реши задачи ниже — за каждую +5 XP, за полный разгром босса — ачивка и бонус.</p>'
|
||||||
|
+'<div class="flag-card"><div class="flag-title">Карта связей понятий</div><div class="flag-help">Кликни по связи — увидишь, как понятия главы связаны.</div><div id="c-concept"></div></div>'+secNav('p9',null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── монтаж sidebar после загрузки ачивки (движок сам строит) ── */
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Feature Context: Контент-движок лаборатории
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
- Лаборатория работает на захардкоженной регистрации (см. PLAN.md Summary).
|
||||||
|
- Ветка `feature/lab-content-engine` создана от `master`.
|
||||||
|
|
||||||
|
## Architecture map (как было ДО рефактора)
|
||||||
|
- `frontend/lab.html` — sim-тела `<div id="sim-xxx">` (inline HTML, ~3000 строк) + 58 `<script>` тегов (4800-4861) + three.js.
|
||||||
|
- `frontend/js/labs/lab-glue.js`:
|
||||||
|
- `_catFilter`, `_disabledSimIds`, `_simModuleDisabled` (вкл/выкл из админки)
|
||||||
|
- `filterSims()`, `renderSims()` (карточки каталога)
|
||||||
|
- preview-хелперы `_grid/_axes/_svg` + ~60 констант `P_*`
|
||||||
|
- массив `SIMS` (821-866), `window.SIMS`/`window.LAB_SIMS`
|
||||||
|
- `frontend/js/labs/lab-init.js`:
|
||||||
|
- объявления переменных симуляций (gSim, pSim, …)
|
||||||
|
- `ALL_SIM_BODIES` / `ALL_CTRL_BARS` (33-48)
|
||||||
|
- `_pauseAllSims()` (54-91), `openSim(id)` if-цепочка (93-160), `closeSim()` (212-258)
|
||||||
|
- `_simShow()`, `_addTouchSupport()` (touch-bridge + ResizeObserver)
|
||||||
|
- объект `THEORY` + `loadTheory()` + `_theoryToggle()`
|
||||||
|
- функции `_openXxx()` (603-756) — единый шаблон: `_simShow('sim-xxx')` + ленивое `new XxxSim(...)` + показ `ctrl-xxx`
|
||||||
|
- `frontend/js/admin/sections/sims.js` — админ-секция (пока только вкл/выкл, `_disabledSimIds`).
|
||||||
|
|
||||||
|
## Загрузочный порядок (КРИТИЧНО)
|
||||||
|
В lab.html: движки `_fx_*`, `_phys_visuals`, `_graph_panel`, `_chem_visuals` грузятся ПЕРЕД симуляциями.
|
||||||
|
`lab-init.js` (4826) грузится ПЕРЕД `lab-glue.js` (4827). `renderSims()` вызывается в конце lab-glue.
|
||||||
|
Некоторые sim-файлы (graph.js) грузятся РАНЬШЕ lab-glue.js → preview `P_*` ещё не определены на момент исполнения их тел.
|
||||||
|
=> В манифестах `preview` поддерживает функцию (ленивое вычисление в renderSims), не только строку.
|
||||||
|
|
||||||
|
## Контракт LabRegistry (Фаза 0)
|
||||||
|
```
|
||||||
|
LabRegistry.register(manifest) // manifest.id уникален; повторная регистрация перезаписывает
|
||||||
|
LabRegistry.get(id) // по base-id (без ':arg')
|
||||||
|
LabRegistry.has(id)
|
||||||
|
LabRegistry.all() // в порядке регистрации
|
||||||
|
LabRegistry.setActive(sim) / stopActive() / destroyActive() // менеджер жизненного цикла
|
||||||
|
```
|
||||||
|
manifest: `{ id, cat, title, desc, preview(string|fn), theory?, bodyId?, mount?(host), open(ctx), stop?(), destroy?(), subject?, grade?, topics? }`
|
||||||
|
|
||||||
|
## Адаптер (Фаза 0): реестр в приоритете, иначе legacy
|
||||||
|
- `renderSims()` — порядок берём из исходного `SIMS`; для id, который есть в реестре, используем манифест (resolve preview), иначе legacy-запись; в конце добавляем registry-only записи, которых нет в SIMS.
|
||||||
|
- `openSim(id)` — `base = id.split(':')[0]`; если `LabRegistry.has(base)` → `stopActive()`; `get(base).open({arg})`; `setActive`; иначе старый if-путь.
|
||||||
|
- `loadTheory(id)` — если `get(base).theory` есть → рендерим из него; иначе `THEORY[base]`.
|
||||||
|
- `closeSim()`/`_pauseAllSims()` — дополнительно `LabRegistry.stopActive()` / `destroyActive()`.
|
||||||
|
|
||||||
|
## RESUME STATE — Phase 4 done (2026-05-30, latest)
|
||||||
|
- Ф4: каталог симуляций в БД. Миграция `042_lab_sims.sql` (таблица lab_sims, сид 40), `backend/src/routes/lab.js` (GET /api/lab/sims auth; PATCH /:id + POST /reorder admin), mount в server.js, 11 тестов, переписан admin/sections/sims.js (убран хардкод ADMIN_SIMS).
|
||||||
|
- enabled зеркалится в legacy app_settings.sim_disabled_ids → lab.html без правок. preview-SVG остаются в коде.
|
||||||
|
- Ревью PASS (без блокеров). route-auth lint чистый. Миграция применена к живой БД. Запушено, remote синхронен.
|
||||||
|
- ВАЖНО: `npm test` = 3 PRE-EXISTING baseline-фейла (НЕ мои; документированы с lab-split). pre-commit хук: BASELINE_FAILS=3, блокирует только при >3. Мои 11 проходят, +0.
|
||||||
|
- ⚠️ Параллельная сессия коммитит в ветку — был 2 behind, rebase прошёл чисто (без пересечений по файлам). Всегда fetch+rebase перед push.
|
||||||
|
- НЕ ПРОВЕРЕНО В БРАУЗЕРЕ: админка «Симуляции» (грузит /api/lab/sims, тумблеры, звезда featured) + исчезновение выключенной симуляции на /lab.
|
||||||
|
- ОСТАЛОСЬ: Фаза 5 (курикулум: lab_sim_links + кнопки «Открыть в лаборатории» в учебнике/теории + связанная теория/задачи на странице sim). subject/grade/featured/tags уже в схеме lab_sims.
|
||||||
|
|
||||||
|
## RESUME STATE — Phase 3 done + FIXED (2026-05-30, ранее)
|
||||||
|
- HEAD=9069d80 (Ф3 + критический фикс). ЗАПУШЕНО, remote синхронен (0 0).
|
||||||
|
- ВАЖНЫЙ УРОК: коммит fc1139f был СЛОМАН — 2 edit'а (_register-all open-обёртка + lab-init Promise-обработка) не применились (упали по отступу old_string), а я запушил, не заметив. Ревью-агент поймал: lab.html убрал eager-скрипты, но open остался синхронным → ReferenceError на клике. Фикс в 9069d80. ПРАВИЛО: после каждого edit проверять `grep -c` маркера; не пушить пакет без поштучной верификации.
|
||||||
|
- ТЕПЕРЬ КОРРЕКТНО: open → LabLoader.ensure(id).then(rawOpen); openSim обрабатывает Promise. E2E vm-harness (click→ensure→load→rawOpen, pendulum/stereo:cube/molphys/alias magnetic) ALL PASS.
|
||||||
|
|
||||||
|
## RESUME STATE — Phase 3 done (исходный, до фикса)
|
||||||
|
- HEAD=70762be (Ф3). Ленивая загрузка кода: старт /lab ~530KB вместо ~2.9MB+600KB three.js (~6×).
|
||||||
|
- Новые файлы: `_loader.js`, `_sim_deps.js` (генерированный манифест). Правки: `_register-all.js`, `lab-init.js`, `lab.html` (eager сокращён до каркаса).
|
||||||
|
- Манифест SIM_DEPS: каждый sim → {open, files[], three}. Инвариант (проверен): файл, определяющий open-функцию, ВСЕГДА в files[] (кроме graph — он eager). Self-heal = страховка.
|
||||||
|
- ⚠️ PUSH: на момент завершения Ф3 окружение глючило (пустой вывод команд); локально 2 коммита ahead (Ф2-docs + Ф3), 0 behind. НУЖНО допушить: `git push origin feature/lab-content-engine` (мог не пройти из-за транзиентного auth — повторить).
|
||||||
|
- ⚠️ НЕ ПРОВЕРЕНО В БРАУЗЕРЕ (см. чеклист в phase-3-lazy-load.md handoff).
|
||||||
|
- ГЕНЕРАТОР МАНИФЕСТА был временным (%TEMP%, удалён). Логика: framework-set + статический анализ provides/refs (комментарии вырезаются) + транзитивное замыкание по ленивым файлам; 3D-set хардкод {crystal,orbitals,stereo,periodic}; EXTRA_DEPS={periodic:[_periodic_data.js]}. При Ф4/5 положить в tools/gen-sim-deps.js.
|
||||||
|
- СЛЕДУЮЩЕЕ: Ф4 (БД lab_sims + API + админка, backend — не трогает lab.html) или Ф5 (курикулум).
|
||||||
|
|
||||||
|
## RESUME STATE — Phase 2 done (2026-05-30, ранее)
|
||||||
|
- Ф2: 40 тел симуляций (~4420 строк) вынесены из lab.html (4880→484 строк) в `frontend/labs-bodies.html`. На месте региона — `#sim-bodies-host` + инлайн-скрипт с СИНХРОННЫМ XHR (`open(...,false)`), который во время парсинга грузит partial и `insertAdjacentHTML('beforebegin')` вставляет тела ДО хоста, затем удаляет хост. Тела присутствуют до DOMContentLoaded → обработчики geometry.js:3207 и порядок init сохранены.
|
||||||
|
- ctrl-бары (#ctrl-*) и #theory-panel ОСТАЛИСЬ в lab.html (они в topbar, не в регионе).
|
||||||
|
- partial раздаётся существующим `express.static(frontendDir)` (server.js:475) — новый роут не нужен.
|
||||||
|
- ГАРАНТИИ (механические, не браузерные): реконструкция before+region+after == оригинал ПОБАЙТОВО; id-мультимножество (newLab−host)+partial == оригинал; 40 sim-body div; node --check OK.
|
||||||
|
- ⚠️ НЕ ПРОВЕРЕНО В БРАУЗЕРЕ. РИСКИ к проверке вручную: (1) sync-XHR может блокироваться при file:// — но тут Express, ок; (2) консольное предупреждение о deprecated sync XHR — безвредно; (3) CSP на инлайн-скрипт — на странице уже есть инлайн-скрипты, должно быть ок; (4) кэш partial (?v=1) — при правках бампать версию.
|
||||||
|
- ПРОВЕРИТЬ: открыть /lab, дождаться каталога, открыть несколько симуляций (graph, pendulum, geometry — у неё DOMContentLoaded-кнопки, stereo:cube, opticsbench), убедиться что canvas рисуется и кнопки работают.
|
||||||
|
- ОТКАТ: `git revert <commit Ф2>` или вернуть регион из labs-bodies.html обратно.
|
||||||
|
- СЛЕДУЮЩЕЕ: Фаза 3 (ленивая загрузка кода) ИЛИ Фаза 4 (БД+админка, backend — без конфликтов с lab.html). Параллельная сессия всё ещё может править lab.html.
|
||||||
|
|
||||||
|
## RESUME STATE — Phase 1 done (2026-05-30, later)
|
||||||
|
- Коммиты: 36c091b → 0888a70 (фикс Ф0) → ebb2a9b (Ф1). HEAD=ebb2a9b, ЗАПУШЕНО, remote синхронен.
|
||||||
|
- Ф1: централизованный `_register-all.js` (data-driven из SIMS+THEORY+OPEN map 40 шт), if-цепочка openSim удалена, _pilots.js удалён, LAB_SIM_ALIASES добавлены. Ревью PASS, vm-harness ALL PASS.
|
||||||
|
- SIMS/THEORY/_pauseAllSims/closeSim/ALL_SIM_BODIES/ALL_CTRL_BARS НАМЕРЕННО оставлены (источники данных + lifecycle-дробовик) — паритет без браузера. Их удаление = Фаза 2 (с ленивым mount).
|
||||||
|
- БЛОКЕР-РИСК: параллельная сессия (biochem/opticsbench) активно правит lab.html и откатывала мои include-правки. Перед Фазой 2 (вынос ~3000 строк из lab.html) — СОГЛАСОВАТЬ, иначе конфликты/потеря работы.
|
||||||
|
- СЛЕДУЮЩЕЕ: пользователю желательно открыть /lab в браузере и кликнуть несколько симуляций (особенно с :arg — стерео-фигуры, оптика-режимы) перед Фазой 2.
|
||||||
|
|
||||||
|
## RESUME STATE (2026-05-30, ранее)
|
||||||
|
- Ветка `feature/lab-content-engine`. Коммиты: 36c091b (Фаза 0, была неполной) → 8f72d68 (фикс 3 блокеров, ЗАПУШЕН).
|
||||||
|
- Первое ревью Фазы 0 = FAIL (3 блокера: _registry.js не подключён, пилоты не зарегистрированы, loadTheory не адаптирован). ВСЕ ТРИ ИСПРАВЛЕНЫ в 8f72d68:
|
||||||
|
- `_registry.js` подключён в lab.html:4799 (после three.js).
|
||||||
|
- `frontend/js/labs/_pilots.js` — регистрирует graph/quadratic/pendulum; подключён в lab.html:4863 (defer, последним).
|
||||||
|
- `loadTheory` (lab-glue.js:951) — реестр в приоритете.
|
||||||
|
- СЛЕДУЮЩИЙ ШАГ: повторное независимое ревью Фазы 0 (агент). Если PASS → Фаза 1.
|
||||||
|
- Фаза 1 решено вести ПО КАТЕГОРИЯМ с коммитами (math→phys→chem→bio→game), legacy удалять в конце.
|
||||||
|
- НЕ ПРОВЕРЕНО в браузере (нет автоматизации). Паритет — статически + ревью.
|
||||||
|
- ВАЖНО: _open* функции глобальны (graph.js:506, quadratic.js:454, pendulum.js:1749). P_* — top-level const в lab-glue.js (доступны cross-script). THEORY — const в lab-init.js. Манифест Фазы1 регистрировать в самом sim-файле, но следить за порядком (preview/theory как fn если файл грузится до lab-glue/lab-init).
|
||||||
|
- УРОК: lab-glue.js/lab-init.js часто перенумеровываются линтером — перед Edit перечитывать. Делать правки ПО ОДНОЙ с проверкой (был сбой пакетного выполнения).
|
||||||
|
|
||||||
|
## Temporary Workarounds
|
||||||
|
- Пилоты (graph/quadratic/pendulum) оставлены в SIMS/THEORY для порядка карточек и единого источника теории; merge перекрывает по id. Удалить в Фазе 1.
|
||||||
|
|
||||||
|
## Known follow-ups (из ревью Фазы 0)
|
||||||
|
- При переключении на LEGACY-симуляцию `LabRegistry._active` не очищается → лишний destroyActive() на неактивной. Безвредно сейчас; очистить `_active` на legacy-open в Фазе 1.
|
||||||
|
|
||||||
|
## Cross-Phase Dependencies
|
||||||
|
- Фаза 1 опирается на ядро реестра из Фазы 0.
|
||||||
|
- Фаза 3 (ленивая загрузка) опирается на манифесты с зависимостями движков (Фаза 1/2).
|
||||||
|
- Фаза 4 (БД) мёржит код-манифесты Фазы 1 с оверрайдами.
|
||||||
|
- Фаза 5 использует поля subject/grade/topics из манифестов.
|
||||||
|
|
||||||
|
## Deep-links (сохранить!)
|
||||||
|
`openSim('stereo:figure')`, `?stereofig=`, обратная совместимость `magnetic/coulomb→emfield`, `thinlens/mirrors/refraction→opticsbench`.
|
||||||
|
|
||||||
|
## Проектные правила (НЕ нарушать)
|
||||||
|
- Иконки: только inline SVG `.ic`, НЕ эмоджи.
|
||||||
|
- Поиск по коду: ast-index, НЕ Grep tool.
|
||||||
|
- БД: встроенный `node:sqlite` DatabaseSync, НЕ better-sqlite3.
|
||||||
|
- Git: коммитить только изменённые файлы.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Feature: Контент-движок лаборатории (симуляции как данные)
|
||||||
|
|
||||||
|
**Branch:** `feature/lab-content-engine`
|
||||||
|
**Base branch:** `master`
|
||||||
|
**Created:** 2026-05-30
|
||||||
|
**Status:** ✅ Complete (все 6 фаз; backend полностью, фронт Ф5 — параллельная сессия; не мёржено в master)
|
||||||
|
**Strategy:** Big Bang
|
||||||
|
**Mode:** Automated
|
||||||
|
**Execution:** Direct
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Превратить захардкоженную в 6 местах регистрацию ~49 симуляций лаборатории в единый
|
||||||
|
декларативный манифест + реестр (`LabRegistry`). Каждая симуляция сама себя регистрирует
|
||||||
|
объектом `{id, cat, title, desc, preview, theory, bodyId/mount, open, stop, destroy, subject, grade, topics}`.
|
||||||
|
Ядро (renderSims/openSim/closeSim/loadTheory) работает с реестром, а не с массивами и
|
||||||
|
if-цепочками. Далее — ленивая загрузка кода, БД-бэкенд с админкой и курикулумная привязка.
|
||||||
|
|
||||||
|
## Build & Test Commands
|
||||||
|
- **Build:** — (фронт без сборки, статика через Express)
|
||||||
|
- **Test:** `cd backend && npm test` (актуально для Фаз 4-5; Фазы 0-3 — статическая проверка + ревью по диффу)
|
||||||
|
- **Lint:** `cd backend && npm run lint:routes` (актуально для Фаз 4-5)
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
- [x] Phase 0: Ядро реестра + адаптер + 3 пилота [domain: frontend] → [subplan](./phase-0-registry-core.md)
|
||||||
|
- [x] Phase 1: Миграция всех симуляций на манифесты [domain: frontend] → [subplan](./phase-1-migrate-all.md)
|
||||||
|
- [x] Phase 2: Тела симуляций вынесены в labs-bodies.html (sync-инъекция) [domain: frontend] → [subplan](./phase-2-lazy-mount.md)
|
||||||
|
- [x] Phase 3: Ленивая загрузка кода симуляций [domain: frontend] → [subplan](./phase-3-lazy-load.md)
|
||||||
|
- [x] Phase 4: Реестр в БД + API + админка [domain: fullstack] → [subplan](./phase-4-db-admin.md)
|
||||||
|
- [x] Phase 5: Курикулумная привязка [domain: fullstack] → [subplan](./phase-5-curriculum.md)
|
||||||
|
|
||||||
|
## Phase Progress Log
|
||||||
|
|
||||||
|
| Phase | Domain | Status | Review | Build | Committed |
|
||||||
|
|-------|--------|--------|--------|-------|-----------|
|
||||||
|
| Phase 0: Ядро реестра | frontend | ✅ Done (fix 0888a70) | ✅ PASS (re-review) | ✅ n/a | ⚠️ local only (push auth fail) |
|
||||||
|
| Phase 1: Миграция всех | frontend | ✅ Done (ebb2a9b) | ✅ PASS | ✅ n/a | ✅ pushed |
|
||||||
|
| Phase 2: Вынос тел | frontend | ✅ Done | ✅ браузер-проверка пройдена | ✅ n/a | ✅ pushed |
|
||||||
|
| Phase 3: Ленивая загрузка | frontend | ✅ Done (201e94e +fix) | ✅ E2E harness ALL PASS | ✅ n/a | ⚠️ нужна браузер-проверка |
|
||||||
|
| Phase 4: БД + админка | fullstack | ✅ Done | ✅ PASS (review) | ✅ 11/11 +0 baseline | ✅ pushed |
|
||||||
|
| Phase 5: Курикулум | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||||
|
|
||||||
|
## Final Review
|
||||||
|
- [x] Per-phase independent reviews (PASS)
|
||||||
|
- [x] Backend tests pass (lab-sims 11/11, lab-links 18/18; +0 к baseline)
|
||||||
|
- [x] route-auth lint: 0 роутов lab.js во флаге
|
||||||
|
- [ ] Merged to `master` (НЕ выполнено — ветка общая, мёрж за пользователем)
|
||||||
|
|
||||||
|
## Notes (Big Bang temporary breakage map)
|
||||||
|
- Фаза 1 может временно ломать каталог/открытие симуляций пока миграция не завершена — устраняется внутри Фазы 1.
|
||||||
|
- Фаза 2 временно меняет структуру lab.html (вынос тел) — устраняется внутри Фазы 2.
|
||||||
|
- Полная работоспособность лаборатории гарантируется после ФИНАЛЬНОЙ фазы.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Phase 0: Ядро реестра + адаптер + 3 пилота
|
||||||
|
|
||||||
|
**Status:** ✅ Done
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** frontend
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Создать `LabRegistry` (реестр + менеджер активной симуляции). Подключить адаптер: ядро
|
||||||
|
лаборатории сначала смотрит в реестр, иначе — старый путь. Мигрировать 3 пилота
|
||||||
|
(graph, quadratic, pendulum) и доказать паритет. Полностью обратимо.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Создать `frontend/js/labs/_registry.js` — `window.LabRegistry` (register/get/has/all + setActive/stopActive/destroyActive). Без эмоджи.
|
||||||
|
- [ ] Подключить `_registry.js` в lab.html ПЕРВЫМ среди labs-скриптов (до graph.js).
|
||||||
|
- [ ] Адаптер `renderSims()` (lab-glue.js): порядок из SIMS, registry-override + resolve preview (string|fn), append registry-only.
|
||||||
|
- [ ] Адаптер `openSim()` (lab-init.js): base-id, registry-first → stopActive/open/setActive; deep-link `:arg` сохранить.
|
||||||
|
- [ ] Адаптер `loadTheory()` (lab-init.js): registry.theory в приоритете, иначе THEORY[base].
|
||||||
|
- [ ] Адаптер `closeSim()`/`_pauseAllSims()`: добавить `LabRegistry.stopActive()`/`destroyActive()`.
|
||||||
|
- [ ] Зарегистрировать 3 пилота в конце lab-init.js (после _openXxx): graph, quadratic, pendulum — с preview-fn, theory-объектом, open=_openXxx, stop/destroy.
|
||||||
|
- [ ] Удалить graph/quadratic/pendulum из legacy `THEORY` и `SIMS` (проверка, что адаптер их подхватывает из реестра).
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- `frontend/js/labs/_registry.js` — новый: ядро реестра.
|
||||||
|
- `frontend/lab.html` — добавить `<script src="/js/labs/_registry.js">` первым (в обоих местах, если дублируется список).
|
||||||
|
- `frontend/js/labs/lab-glue.js` — renderSims адаптер; убрать 3 пилота из SIMS.
|
||||||
|
- `frontend/js/labs/lab-init.js` — openSim/loadTheory/closeSim/_pauseAllSims адаптеры; регистрация 3 пилотов; убрать 3 пилота из THEORY.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Каталог отображает все симуляции в прежнем порядке; 3 пилота открываются и работают идентично.
|
||||||
|
- Остальные 46 симуляций открываются по-старому (legacy путь не сломан).
|
||||||
|
- Deep-links и обратная совместимость id работают.
|
||||||
|
- Нет дублей карточек (пилот не показан дважды).
|
||||||
|
- Нет эмоджи; иконки `.ic`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Порядок загрузки: см. CONTEXT.md. preview как функция спасает от undefined P_*.
|
||||||
|
- `_disabledSimIds` фильтрация должна продолжать работать для registry-записей.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Адаптер не ломает legacy симуляции
|
||||||
|
- [ ] Паритет 3 пилотов (open/stop/close/theory/preview)
|
||||||
|
- [ ] Соблюдены конвенции проекта (no emoji, .ic)
|
||||||
|
- [ ] Нет дублирования карточек
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
- Ядро `LabRegistry` готово (`frontend/js/labs/_registry.js`): register/get/has/all + setActive/stopActive/destroyActive + resolvePreview. Подключено первым.
|
||||||
|
- Адаптер активен в renderSims (lab-glue.js) и openSim/loadTheory/_pauseAllSims/closeSim (lab-init.js): реестр в приоритете, иначе legacy.
|
||||||
|
- Пилоты graph/quadratic/pendulum зарегистрированы в КОНЦЕ lab-init.js (после _openXxx). preview — ленивая функция (P_* из lab-glue, грузится позже). theory ссылается на объекты THEORY (единый источник).
|
||||||
|
- Пилоты НЕ удалены из SIMS/THEORY (сохранены для порядка и единого источника); merge перекрывает их по id. В Фазе 1 удалить legacy полностью.
|
||||||
|
- РЕВЬЮ-WARNING для Фазы 1: при переключении на LEGACY-симуляцию `_active` в реестре не очищается → следующий closeSim вызовет destroyActive() на уже неактивной. Для пилотов безвредно (idempotent stop). Очистить `_active` на legacy-open при миграции.
|
||||||
|
- Паттерн манифеста для Фазы 1: `{id,cat,title,desc,preview(fn|str),theory,open,stop,destroy}`. Регистрировать в самом sim-файле; следить за порядком загрузки (preview как fn спасает).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Phase 1: Миграция всех симуляций на манифесты
|
||||||
|
|
||||||
|
**Status:** ✅ Done (ebb2a9b, reviewed PASS, pushed)
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** frontend
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Перевести все ~49 симуляций на сам/регистрацию через `LabRegistry`. Перенести данные
|
||||||
|
(catalogue meta, preview, theory) и поведение (open/stop/destroy) в манифесты. Удалить
|
||||||
|
legacy-структуры. Сохранить глобальные имена через shim.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Для каждой симуляции зарегистрировать манифест (метаданные из SIMS, preview из P_*, theory из THEORY, open/stop/destroy из _openXxx + _pauseAllSims/closeSim веток).
|
||||||
|
- [ ] Удалить массив `SIMS` (lab-glue.js) и объект `THEORY` (lab-init.js).
|
||||||
|
- [ ] Удалить if-цепочку `openSim`, `_pauseAllSims`, switch в `closeSim`, `ALL_SIM_BODIES`/`ALL_CTRL_BARS`.
|
||||||
|
- [ ] lab-init.js усохнуть до generic-логики (openSim/closeSim через реестр).
|
||||||
|
- [ ] Shim глобальных имён (gSim, pSim, …) — их дёргают deep-link/поиск/инлайн-обработчики.
|
||||||
|
- [ ] Сохранить обратную совместимость id (magnetic/coulomb→emfield, thinlens/mirrors/refraction→opticsbench, stereo:fig, hydrostatics:arg, molphys:arg, chemistry:arg, dynamics:arg, emfield:mode, opticsbench:mode).
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- Все `frontend/js/labs/*.js` симуляции — добавить `LabRegistry.register(...)`.
|
||||||
|
- `frontend/js/labs/lab-glue.js`, `frontend/js/labs/lab-init.js` — удалить legacy.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Все симуляции открываются/работают как раньше (паритет).
|
||||||
|
- Удалены все 6 точек дублирования из CONTEXT.md.
|
||||||
|
- Deep-links и алиасы работают.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Мигрировать пачками (по категориям) с проверкой паритета после каждой пачки (Big Bang допускает временную поломку между пачками).
|
||||||
|
- Превью с зависимостями (random в P_ELECTROLYSIS) перенести как есть.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Ни одна симуляция не потеряна
|
||||||
|
- [ ] Глобальные shim'ы на месте
|
||||||
|
- [ ] Алиасы/deep-links работают
|
||||||
|
- [ ] Legacy полностью удалён
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
- РЕАЛИЗАЦИЯ ОТЛИЧАЕТСЯ ОТ ИСХОДНОГО ПЛАНА (осознанно): вместо ручного переписывания манифестов в каждом sim-файле сделана централизованная data-driven регистрация в `frontend/js/labs/_register-all.js`. Манифесты строятся из существующих SIMS (метаданные+preview) + THEORY (теория) + карта OPEN (40 обёрток над глобальными _openXxx). Это структурно гарантирует паритет каталога и диспетчеризации.
|
||||||
|
- УДАЛЕНО: if-цепочка openSim (~60 строк), _pilots.js. ДОБАВЛЕНО: LAB_SIM_ALIASES (magnetic/coulomb/thinlens/mirrors/refraction).
|
||||||
|
- НЕ УДАЛЕНО (осознанно, для паритета без браузера): SIMS (lab-glue) и THEORY (lab-init) остаются ИСТОЧНИКАМИ данных; _pauseAllSims()/closeSim() «дробовик» и ALL_SIM_BODIES/ALL_CTRL_BARS остаются — управляют скрытием тел и остановкой. Поэтому stop/destroy в манифестах НЕ заданы.
|
||||||
|
- ОСТАТОК ИЗ ПЛАНА (перенесено в Фазу 2+): полное удаление SIMS/THEORY/ALL_SIM_BODIES/ALL_CTRL_BARS и сворачивание lifecycle в манифесты (stop/destroy per-sim) — делать вместе с ленивым mount (Фаза 2), т.к. требует знания тел симуляций. Глобальные имена (gSim, pendSim…) НЕ трогались — shim не понадобился.
|
||||||
|
- ВЕРИФИКАЦИЯ: исполняемый vm-harness (40 регистраций + dispatch + алиасы + :arg) ALL PASS; независимое ревью PASS (coverage 40/40, dispatch parity, node --check). В БРАУЗЕРЕ НЕ ПРОВЕРЕНО.
|
||||||
|
- РИСК ДЛЯ ФАЗЫ 2: параллельная сессия активно правит lab.html (откатывала мои правки include дважды). Фаза 2 = вынос ~3000 строк ИЗ lab.html → высокий риск конфликтов. Координировать.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Phase 2: Тела симуляций как шаблоны + ленивый mount
|
||||||
|
|
||||||
|
**Status:** ✅ Done (3f99d1b) — браузер-проверка пройдена (пользователь подтвердил: симуляции работают)
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** frontend
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Вынести inline-HTML тел симуляций (`<div id="sim-xxx">`, ~3000 строк) из lab.html в
|
||||||
|
манифесты: `mount(host)` создаёт DOM лениво при первом открытии. lab.html худеет.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Добавить в манифест поле `mount(host)` (или `bodyHtml`) — строит/возвращает тело симуляции.
|
||||||
|
- [ ] Ядро: при первом open — если тело не смонтировано, вызвать mount() в контейнер `#lab-sim`.
|
||||||
|
- [ ] Перенести разметку каждого `sim-xxx` тела + его `ctrl-xxx` бара из lab.html в соответствующий модуль.
|
||||||
|
- [ ] Удалить вынесенные блоки из lab.html.
|
||||||
|
- [ ] Сохранить id элементов (canvas ids, ctrl ids) — на них завязаны Sim-классы.
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- Все `frontend/js/labs/*.js` — добавить mount/bodyHtml.
|
||||||
|
- `frontend/lab.html` — удалить inline тела.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Все симуляции монтируются и работают.
|
||||||
|
- lab.html значительно меньше (~3000 строк вынесено).
|
||||||
|
- Повторное открытие не дублирует DOM.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Theory-panel и общий sim-topbar остаются в lab.html.
|
||||||
|
- KaTeX/lucide ре-инициализация после mount при необходимости.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Нет дублей DOM при повторном open
|
||||||
|
- [ ] id элементов сохранены
|
||||||
|
- [ ] Все тела перенесены
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
<!-- заполнить после фазы -->
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Phase 3: Ленивая загрузка кода симуляций
|
||||||
|
|
||||||
|
**Status:** ✅ Done (70762be) — vm-harness + owner-in-files инвариант пройдены; нужна браузер-проверка
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** frontend
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Грузить тяжёлый код симуляции и движков по клику, а не 58 скриптов + three.js сразу.
|
||||||
|
Лёгкий манифест каталога загружается сразу.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Вынести лёгкие метаданные каталога (id/cat/title/desc/preview) в отдельный `sims.manifest.js`, грузимый сразу.
|
||||||
|
- [ ] Тяжёлый код симуляции (Sim-класс + open/mount) грузить динамически при openSim (инъекция script или import()).
|
||||||
|
- [ ] Объявить зависимости движков в манифесте (`deps: ['_fx_core','_phys_visuals',...]`); загрузчик резолвит и грузит до кода симуляции, кешируя загруженное.
|
||||||
|
- [ ] three.js грузить только для 3D-симуляций (stereo).
|
||||||
|
- [ ] Лоадер с дедупликацией (один и тот же файл не грузится дважды).
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- `frontend/js/labs/_loader.js` — новый: динамический загрузчик + резолв зависимостей.
|
||||||
|
- `frontend/js/labs/sims.manifest.js` — новый: лёгкий каталог.
|
||||||
|
- `frontend/lab.html` — убрать массовые `<script>`, оставить ядро (api, registry, loader, manifest).
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Первый рендер каталога без загрузки кода симуляций.
|
||||||
|
- Открытие симуляции догружает её код+движки и работает.
|
||||||
|
- three.js грузится только для 3D.
|
||||||
|
- Заметное падение объёма стартовой загрузки lab.html.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Учесть defer-скрипты (solutions/organic/periodic/qualanalysis).
|
||||||
|
- Кеш загруженных модулей в Map.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Нет двойной загрузки
|
||||||
|
- [ ] Зависимости движков соблюдены
|
||||||
|
- [ ] Старт лаборатории легче
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
- РЕАЛИЗОВАНО: каркас (~530KB) грузится eager; код симуляций (~2.5MB) + three.js (~600KB) — лениво по клику. Старт /lab легче в ~6 раз.
|
||||||
|
- Файлы: `_loader.js` (LabLoader.ensure + кеш + self-heal), `_sim_deps.js` (СГЕНЕРИРОВАННЫЙ манифест SIM_DEPS + LAB_LAZY_FILES), правки `_register-all.js` (open→ensure.then), `lab-init.js` (openSim обрабатывает Promise), `lab.html` (eager сокращён до 12 labs-скриптов + framework).
|
||||||
|
- EAGER framework: _registry,_loader,_sim_deps,_fx_*,_graph_panel,_phys_visuals,_chem_visuals,_util,graph.js,lab-init,lab-glue,_register-all. graph.js eager (даёт GRID для 15 сим). _periodic_data — ЛЕНИВЫЙ (dep periodic).
|
||||||
|
- three.js — лениво, только для crystal/orbitals/stereo/periodic (THREE_SIMS).
|
||||||
|
- SELF-HEAL: если после загрузки манифестных файлов window[openName] не функция → грузятся ВСЕ ленивые файлы. Поэтому ошибка манифеста не ломает симуляцию (худший случай = поведение до Ф3).
|
||||||
|
- РЕГЕНЕРАЦИЯ МАНИФЕСТА: скрипт-генератор был временным (в %TEMP%), НЕ в репо. При добавлении/переименовании симуляции: либо вручную дописать SIM_DEPS в _sim_deps.js, либо восстановить генератор (логика в CONTEXT). СТОИТ положить генератор в tools/ при Фазе 4/5.
|
||||||
|
- БРАУЗЕР-ПРОВЕРКА (обязательна): открыть /lab → Network: на старте НЕ должно быть 45 sim-js и three.js; кликнуть pendulum (грузит pendulum.js), molphys (gas+states+brownian+diffusion), stereo (three.js+stereo.js), periodic (_periodic_data+periodic+three), chemistry (5 файлов), geometry, opticsbench; проверить deep-link stereo:cube, opticsbench:mirror, alias #magnetic.
|
||||||
|
- РИСК для Ф4/5: lab.html сильно изменён; параллельные сессии правят его — fetch+проверка перед работой.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Phase 4: Реестр в БД + API + админка
|
||||||
|
|
||||||
|
**Status:** ✅ Done — review PASS, 11 тестов, миграция применена, запушено
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** fullstack
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Хранить оверрайды каталога в БД, мёржить с код-манифестами, управлять каталогом из
|
||||||
|
админки (вкл/выкл, порядок, теги, рекомендуемые).
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Миграция БД: таблица `lab_sims` (id PK, title, cat, subject, grade, desc, enabled, sort, topic_id, textbook_ref, flags JSON, updated_at). Через `node:sqlite` DatabaseSync.
|
||||||
|
- [ ] Backend route `GET /api/lab/sims` — отдаёт мёрж: код-манифест (база) + БД-оверрайды.
|
||||||
|
- [ ] Backend admin routes: upsert/enable/disable/reorder/tag (под RBAC admin).
|
||||||
|
- [ ] Frontend: каталог берёт enabled/order/теги из `/api/lab/sims` (с фолбэком на код-манифест офлайн).
|
||||||
|
- [ ] Расширить `frontend/js/admin/sections/sims.js`: список, вкл/выкл, drag-reorder, теги, «рекомендуемые».
|
||||||
|
- [ ] Сохранить совместимость с `_disabledSimIds`.
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- `backend/src/db/migrations/0XX_lab_sims.sql` — новая миграция.
|
||||||
|
- `backend/src/routes/lab.js` (или расширить существующий) — API.
|
||||||
|
- `backend/src/server.js` — подключить роут (если новый файл).
|
||||||
|
- `frontend/js/admin/sections/sims.js` — расширить админку.
|
||||||
|
- `frontend/js/labs/_loader.js`/manifest — учитывать БД-данные.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- `npm test` зелёный; `npm run lint:routes` без ошибок (auth на роутах).
|
||||||
|
- Админ может вкл/выкл/переупорядочить/тегировать симуляцию, изменения видны в каталоге.
|
||||||
|
- Офлайн/без БД — фолбэк на код-манифест.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- RBAC: мутации только admin. Чтение каталога — для роли с доступом к лаборатории.
|
||||||
|
- Не дублировать данные: код-манифест = источник базовых полей; БД = оверрайды/доп.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Миграция идемпотентна
|
||||||
|
- [ ] Роуты под auth (lint:routes)
|
||||||
|
- [ ] Мёрж корректен, фолбэк работает
|
||||||
|
- [ ] Тесты проходят
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
- РЕАЛИЗОВАНО: таблица `lab_sims` (миграция 042), `backend/src/routes/lab.js` (GET /api/lab/sims + PATCH /:id + POST /reorder), монтирование в server.js (require:58, mount:181), 11 тестов `lab-sims.test.js`, переписан `frontend/js/admin/sections/sims.js` (убран ADMIN_SIMS).
|
||||||
|
- ИСТОЧНИК ИСТИНЫ каталога теперь БД (lab_sims). preview-SVG остаются в коде. Поля subject/grade/featured/tags ГОТОВЫ в схеме и API — Фаза 5 их наполнит (курикулум) и фронт /lab может начать их потреблять.
|
||||||
|
- СОВМЕСТИМОСТЬ: enabled зеркалится в app_settings.sim_disabled_ids, поэтому lab.html (читает /api/settings/sims) скрывает выключенные без правок фронта. Каталог /lab пока НЕ читает /api/lab/sims (рендерится из кода-реестра+SIMS) — это опционально для Фазы 5: можно подтянуть порядок/featured/теги из БД.
|
||||||
|
- ТЕСТЫ: `npm test` имеет 3 PRE-EXISTING baseline-фейла (документированы с lab-split, не связаны с этой работой). pre-commit хук толерантен (BASELINE_FAILS=3). Мои 11 тестов проходят, +0 к фейлам.
|
||||||
|
- БРАУЗЕР-ПРОВЕРКА (желательно для Ф4): зайти в админку → раздел «Симуляции»: список грузится из API, тумблеры вкл/выкл и звёзда «рекомендуемая» работают; на /lab выключенная симуляция исчезает из каталога.
|
||||||
|
- ⚠️ ПАРАЛЛЕЛЬНАЯ СЕССИЯ активно коммитит в эту ветку (chemistry-8 и др.) — fetch+rebase перед push (в этой фазе было 2 behind, rebase прошёл чисто, без пересечений по файлам).
|
||||||
|
- Для Фазы 5: связи sim ↔ §учебника/тема/kmap можно класть в новую таблицу `lab_sim_links`; subject/grade уже есть в lab_sims для грубой привязки.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Phase 5: Курикулумная привязка
|
||||||
|
|
||||||
|
**Status:** ⬜ Not Started
|
||||||
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
|
**Domain:** fullstack
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Связать симуляции с учебной программой: § учебника, узел knowledge-map, тема банка
|
||||||
|
вопросов. Двусторонняя навигация.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
- [ ] Схема связей: использовать поля манифеста (subject/grade/topics) + таблицу связей `lab_sim_links` (sim_id, kind[textbook|topic|kmap|question], ref_id).
|
||||||
|
- [ ] API: `GET /api/lab/sims/:id/related` — связанные § / темы / задачи.
|
||||||
|
- [ ] Frontend учебник/теория: кнопка «Открыть в лаборатории» в § (deep-link openSim).
|
||||||
|
- [ ] Frontend knowledge-map: узел темы → ссылка на симуляцию.
|
||||||
|
- [ ] Страница симуляции: блок «Связанная теория и задачи».
|
||||||
|
- [ ] Админка: редактирование связей симуляции.
|
||||||
|
|
||||||
|
## Files to Modify/Create
|
||||||
|
- `backend/src/db/migrations/0XX_lab_sim_links.sql`
|
||||||
|
- `backend/src/routes/lab.js` — related endpoint.
|
||||||
|
- `frontend/textbooks.html` / theory / учебник-рендер — кнопки в §.
|
||||||
|
- `frontend/knowledge-map.html` — ссылки с узлов.
|
||||||
|
- `frontend/lab.html` — блок связей на странице sim.
|
||||||
|
- `frontend/js/admin/sections/sims.js` — редактор связей.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Из § учебника можно открыть нужную симуляцию.
|
||||||
|
- На странице симуляции видны связанные теория/задачи.
|
||||||
|
- Узлы knowledge-map ведут на симуляции.
|
||||||
|
- `npm test` зелёный, роуты под auth.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Привязки опциональны: отсутствие связей не ломает страницы.
|
||||||
|
- Переиспользовать существующие topic_id банка вопросов и структуру учебников.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Навигация в обе стороны работает
|
||||||
|
- [ ] Пустые связи не ломают UI
|
||||||
|
- [ ] Роуты под auth, тесты проходят
|
||||||
|
|
||||||
|
## Handoff to Next Phase
|
||||||
|
<!-- финальная фаза -->
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
# План реализации: Химия 8 (Беларусь) — интерактивный наглядный учебник
|
||||||
|
|
||||||
|
> Цель: создать **с нуля** интерактивный наглядный учебник по всей программе 8 класса
|
||||||
|
> в **современной архитектуре hub + главы** (как Физика 7–11 / Алгебра / Геометрия —
|
||||||
|
> НЕ как легаси-монолит `chemistry_9.html`), на уровне их качества, с поправкой на
|
||||||
|
> содержание 8 класса: **количественные понятия (моль, M, Vm, расчёты по уравнениям),
|
||||||
|
> классы неорганических соединений, периодический закон, строение атома, химическая
|
||||||
|
> связь, ОВР, растворы.**
|
||||||
|
>
|
||||||
|
> **Архитектура (утверждена):** `chemistry_8_hub.html` (хаб-каталог глав) + **7 файлов глав**
|
||||||
|
> (вводный раздел + 6 глав книги). В БД — родитель `chemistry-8` + 7 детей с `parent_slug`.
|
||||||
|
> Каждая глава — самостоятельная страница со своими § (см. карту ниже), модульный
|
||||||
|
> CSS/JS на предмет. Единый стандарт с планом Химии 9 ([[plans/textbooks-9/PLAN_CHEMISTRY_9.md]]).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Источник
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|----------|----------|
|
||||||
|
| Книга | `himiya_8kl_shimanovich_rus_2018 (1).pdf` |
|
||||||
|
| Авторы | Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н. |
|
||||||
|
| Изд. | Минск, «Народная асвета», 2018, 243 с. (тираж 116 000) |
|
||||||
|
| Структура | **Вводный раздел + 6 глав, 52 §, 4 лабораторных опыта, 4 практические работы** |
|
||||||
|
| Справочные таблицы | ПСХЭ Менделеева, таблица растворимости, ряд активности металлов (форзацы) |
|
||||||
|
|
||||||
|
PDF лежит в `G:\Dev\Тесты\Методички\тест_6 класс\Книги\`. Оглавление — стр. 238–239 PDF.
|
||||||
|
|
||||||
|
> **Важные отличия от Химии 9:**
|
||||||
|
> 1. Страниц химии-8 **ещё нет** — строим с нуля. Каркас берём у **современных hub-учебников**
|
||||||
|
> (`physics_9_hub.html` + `physics_9_chN.html`), а НЕ у легаси-монолита `chemistry_9.html`.
|
||||||
|
> 2. Описательной химии металлов/неметаллов в 8 классе **нет** (это 9 класс) — акцент на
|
||||||
|
> количественные расчёты, классификацию веществ, строение атома и химическую связь, ОВР.
|
||||||
|
>
|
||||||
|
> **Соответствие «раздел книги → файл главы → slug»:**
|
||||||
|
>
|
||||||
|
> | Раздел книги | § | Файл | slug | Цвет |
|
||||||
|
> |---|---|---|---|---|
|
||||||
|
> | Вводный: Количественные понятия | 1–9 | `chemistry_8_intro.html` | `chemistry-8-intro` | amber |
|
||||||
|
> | Гл.1 Классы неорг. соединений | 10–23 | `chemistry_8_ch1.html` | `chemistry-8-ch1` | teal |
|
||||||
|
> | Гл.2 Периодический закон и ПСХЭ | 24–28 | `chemistry_8_ch2.html` | `chemistry-8-ch2` | indigo |
|
||||||
|
> | Гл.3 Строение атома | 29–35 | `chemistry_8_ch3.html` | `chemistry-8-ch3` | blue |
|
||||||
|
> | Гл.4 Химическая связь | 36–41 | `chemistry_8_ch4.html` | `chemistry-8-ch4` | green |
|
||||||
|
> | Гл.5 ОВР | 42–45 | `chemistry_8_ch5.html` | `chemistry-8-ch5` | deep-orange |
|
||||||
|
> | Гл.6 Растворы | 46–52 | `chemistry_8_ch6.html` | `chemistry-8-ch6` | cyan |
|
||||||
|
>
|
||||||
|
> Хаб: `chemistry_8_hub.html` / slug `chemistry-8` (родитель в каталоге).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📗 ПОЛНАЯ КАРТА СОДЕРЖАНИЯ (52 §)
|
||||||
|
|
||||||
|
Колонка **«Интерактив»** — главный наглядный элемент сверх текста (минимум 1 «звёздный»
|
||||||
|
виджет на §; полный набор — в стандарте ниже).
|
||||||
|
|
||||||
|
### ВВОДНЫЙ РАЗДЕЛ. Повторение курса 7 класса. Количественные понятия в химии (§§1–9) — *amber*
|
||||||
|
| § | Тема | Ключ | Интерактив (звёздный виджет) |
|
||||||
|
|---|------|------|------------------------------|
|
||||||
|
| §1 | Атомы. Химические элементы. Относительная атомная масса | $Z$, символ, $A_r$ | **miniPeriodic** + поиск $A_r$ по элементу; модель атома |
|
||||||
|
| §2 | Молекулы. Простые/сложные вещества. Химические формулы. $M_r$ | формула, индексы, $M_r=\sum A_r$ | **Конструктор формул** + калькулятор $M_r$ (biochem-core) |
|
||||||
|
| §3 | Химическое количество вещества | понятие «порция», $n$ | Визуализация «порции вещества» (частицы → моль) |
|
||||||
|
| §4 | Моль — единица количества вещества. Постоянная Авогадро | $N=n\cdot N_A$, $N_A=6{,}02\cdot10^{23}$ | **Счётчик частиц** $N\leftrightarrow n$, масштаб $N_A$ |
|
||||||
|
| §5 | Молярная масса. Молярный объём газов | $M$ (г/моль), $V_m=22{,}4$ л/моль (н.у.) | Калькулятор $M$ + газовая модель ($V_m$) |
|
||||||
|
| §6 | Вычисление $n$ по $m$ и $m$ по $n$ | $n=\dfrac{m}{M}$ | **Треугольник $n$–$m$–$M$** (интерактивный калькулятор-тренажёр) |
|
||||||
|
| §7 | Вычисление $n$ газа по $V$ и $V$ по $n$ | $n=\dfrac{V}{V_m}$ | Калькулятор $V=n\cdot V_m$ + связка $m$–$n$–$V$–$N$ |
|
||||||
|
| §8 | Химические реакции | признаки, закон сохранения массы, балансировка | **Балансировщик уравнений** (анимация коэффициентов) + классификатор типов реакций |
|
||||||
|
| §9 | Количественные расчёты по уравнениям реакций | стехиометрия, мольные отношения | **sim `stoichiometry`** + пошаговый решатель «дано → по уравнению» |
|
||||||
|
|
||||||
|
**Практическая работа 1** (после §7): «Химическое количество вещества».
|
||||||
|
|
||||||
|
### ГЛАВА 1. Важнейшие классы неорганических соединений (§§10–23) — *teal/cyan*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §10 | Оксиды. Состав и классификация | $Э_xO_y$; осн./кисл./амфот./несолеобр. | **Классификатор оксидов** (drag формулы → класс) + конструктор формул оксидов по валентности |
|
||||||
|
| §11 | Химические свойства оксидов | осн.оксид+кислота/вода; кисл.оксид+щёлочь/вода | **Матрица реакций оксидов** (`chemEq`, признаки) |
|
||||||
|
| §12 | Получение и применение оксидов | горение, разложение; применение | Схемы получения + инфографика применения |
|
||||||
|
| §13 | Кислоты. Состав и классификация | $H_xAc$; бескисл./кислородсод., основность | **Классификатор кислот** + `indicatorScale` (лакмус/метилоранж) |
|
||||||
|
| §14 | Химические свойства кислот | + Me (ряд активности), + осн.оксид, + основание, + соль | **Реакции кислот** (4 типа) + ряд активности + `indicatorScale` |
|
||||||
|
| §15 | Получение и применение кислот | кисл.оксид+вода, соль+кислота | Схемы получения + инфографика |
|
||||||
|
| §16 | Основания | $Me(OH)_n$; щёлочи/нерастворимые | Конструктор $Me(OH)_n$ + `indicatorScale` (фенолфталеин малиновый) |
|
||||||
|
| §17 | Химические свойства оснований | нейтрализация, +кисл.оксид, +соль, разложение | **Реакция нейтрализации** (анимация) + `indicatorScale` |
|
||||||
|
| §18 | Получение и применение оснований | Me+вода, щёлочь+соль | Схемы; **Лаб.1**: получение нерастворимого основания (`testTube` $Cu(OH)_2$↓ голубой) |
|
||||||
|
| §19 | Соли. Состав и классификация | катион×анион; средние/кислые/основные | **Конструктор солей** (катион×анион) + `solubilityTable` |
|
||||||
|
| §20 | Химические свойства солей | РИО (↓↑), соль+Me (ряд активности) | `solubilityTable` + предсказатель РИО; **Лаб.2**: соли+металлы |
|
||||||
|
| §21 | Получение и применение солей | 8+ способов получения | **Матрица способов получения солей** |
|
||||||
|
| §22 | Взаимосвязь между классами неорг. веществ | генетическая связь Me/неMe → оксид → гидроксид → соль | **Генетическая карта-граф** (интерактивные переходы) |
|
||||||
|
| §23 | Решение расчётных задач по теме | расчёты по классам, по уравнениям | **sim `stoichiometry`** + тренажёр расчётов |
|
||||||
|
|
||||||
|
**Лаб. опыт 1** (после §18): получение нерастворимого основания.
|
||||||
|
**Практическая работа 2** (после §18): изучение реакции нейтрализации.
|
||||||
|
**Лаб. опыт 2** (после §20): взаимодействие растворов солей с металлами.
|
||||||
|
**Практическая работа 3** (после §22): решение экспериментальных задач.
|
||||||
|
|
||||||
|
### ГЛАВА 2. Периодический закон и периодическая система (§§24–28) — *indigo/violet*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §24 | Систематизация химических элементов | ранние классификации, металлы/неметаллы | Сортировщик элементов (Me/неMe/амфот.) |
|
||||||
|
| §25 | Понятие об амфотерности | $Zn(OH)_2, Al(OH)_3$ + кислота **и** + щёлочь | **Амфотерность** (`testTube`, обе реакции); **Лаб.3**: гидроксид цинка |
|
||||||
|
| §26 | Естественные семейства элементов | щелочные, ЩЗМ, галогены, инертные | **miniPeriodic** — подсветка семейств, тренды свойств |
|
||||||
|
| §27 | Периодический закон Д. И. Менделеева | формулировка, периодичность | **Демонстрация периодичности** (карточки-раскладка элементов) |
|
||||||
|
| §28 | Периодическая система химических элементов | период/группа/подгруппа, структура | **Интерактивная ПСХЭ** (sim `periodic`) + разбор структуры |
|
||||||
|
|
||||||
|
**Лаб. опыт 3** (после §25): получение гидроксида цинка и изучение амфотерных свойств.
|
||||||
|
|
||||||
|
### ГЛАВА 3. Строение атома и периодичность свойств (§§29–35) — *blue*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §29 | Строение атома. Атомный номер | ядро ($p^+,n^0$) + $e^-$, $Z$ | **Модель атома** (sim `bohratom`) — сборка по $Z$ |
|
||||||
|
| §30 | Массовое число атома. Нуклиды | $A=Z+N$, нуклид | Калькулятор $A=Z+N$ + конструктор нуклида |
|
||||||
|
| §31 | Изотопы. Явление радиоактивности | одинаковый $Z$, разный $N$; распад | Изотопы-конструктор + sim `radioactive` + расчёт $A_r$ по изотопам |
|
||||||
|
| §32 | Состояние электронов. Электронное облако. Орбиталь | $s,p,d$ орбитали, форма облака | **3D-облака орбиталей** (sim `orbitals`) |
|
||||||
|
| §33 | Строение электронных оболочек атомов | уровни, $2n^2$, конфигурация | **Конструктор электронной конфигурации** (`orbitalDiagram`, заполнение) |
|
||||||
|
| §34 | Периодичность изменения свойств атомов | радиус, ЭО, металличность по периоду/группе | **Графики трендов** (slider период/группа → свойство) |
|
||||||
|
| §35 | Характеристика элемента по положению в ПС | алгоритм «паспорта» элемента | **Генератор «паспорта элемента»** (пошагово) |
|
||||||
|
|
||||||
|
### ГЛАВА 4. Химическая связь (§§36–41) — *green*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §36 | Природа химической связи | октет, энергия связи, устойчивость | Анимация «почему атомы соединяются» |
|
||||||
|
| §37 | Ковалентная связь | общие электронные пары, схемы Льюиса | **Конструктор e-пар** + структурные формулы (biochem-core) |
|
||||||
|
| §38 | Неполярная и полярная ков. связь. ЭО | $\Delta$ЭО → полярность, диполь | **Slider ЭО → тип связи** + диполь; **Лаб.4**: модели молекул (biochem-core 3D) |
|
||||||
|
| §39 | Ионная связь | передача $e^-$, ионная решётка | **Анимация $Na\to Cl$** + решётка $NaCl$ |
|
||||||
|
| §40 | Металлическая связь. Межмолекулярное взаимодействие | «электронный газ», водородная связь | **Модель электронного газа** + водородная связь |
|
||||||
|
| §41 | Кристаллическое состояние вещества | 4 типа решёток → свойства | **4 типа решёток** (3D) + связь «тип → свойства» |
|
||||||
|
|
||||||
|
**Лаб. опыт 4** (после §38): составление моделей молекул.
|
||||||
|
|
||||||
|
### ГЛАВА 5. Окислительно-восстановительные реакции (§§42–45) — *deep-orange*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §42 | Степень окисления | правила, расчёт по формуле | **Калькулятор степени окисления** (любая формула) |
|
||||||
|
| §43 | Процессы окисления и восстановления | отдача/приём $e^-$, окислитель/восстановитель | Визуализация переноса $e^-$ |
|
||||||
|
| §44 | Окислительно-восстановительные реакции | метод электронного баланса | **Балансировщик ОВР** (пошаговый e-баланс) |
|
||||||
|
| §45 | ОВР вокруг нас | горение, коррозия, дыхание, батарейки | Инфографика-исследование ОВР в жизни |
|
||||||
|
|
||||||
|
### ГЛАВА 6. Растворы (§§46–52) — *cyan*
|
||||||
|
| § | Тема | Ключ | Интерактив |
|
||||||
|
|---|------|------|------------|
|
||||||
|
| §46 | Смеси веществ | однородные/неоднородные, разделение | Классификатор смесей + методы разделения |
|
||||||
|
| §47 | Растворение веществ в воде | гидратация, тепловой эффект | **Анимация растворения** (`dissociationAnim`) |
|
||||||
|
| §48 | Характеристики растворимости веществ | $s=f(t)$, насыщ./ненасыщ. | **График растворимости** + `solubilityTable` |
|
||||||
|
| §49 | Качественные характеристики состава растворов | насыщ./ненасыщ./разб./конц. | Качественная шкала «крепости» раствора |
|
||||||
|
| §50 | Количественные характеристики. Массовая доля | $w=\dfrac{m_{в-ва}}{m_{р-ра}}$ | **Калькулятор $w$** (sim `solutions`) |
|
||||||
|
| §51 | Молярная концентрация растворённых веществ | $c=\dfrac{n}{V}$, разбавление, смешение | **Калькулятор $c$** + разбавление/смешение |
|
||||||
|
| §52 | Вода и растворы в жизни и деятельности человека | значение, очистка, быт | Инфографика-исследование |
|
||||||
|
|
||||||
|
**Практическая работа 4** (после §51): приготовление раствора с заданной $w$ и $c$.
|
||||||
|
|
||||||
|
**Итого**: 52 §, вводный раздел + 6 глав, **4 лаб. опыта** (§18, §20, §25, §38), **4 практические работы** (§7, §18, §22, §51).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚗️ ХИМИЧЕСКИЙ СТАНДАРТ КАЧЕСТВА
|
||||||
|
|
||||||
|
### A. Движки и переиспользуемые активы (всё уже есть в проекте)
|
||||||
|
|
||||||
|
| Что нужно | Берём из | Файл / id |
|
||||||
|
|-----------|----------|-----------|
|
||||||
|
| Парсинг формул, $M$/$M_r$, формула Хилла | biochem-core | `frontend/js/biochem-core.js` ✅ |
|
||||||
|
| 2D/3D шаростержневые модели, VSEPR | biochem-core | `frontend/js/biochem-core.js` ✅ |
|
||||||
|
| Интерактивная ПСХЭ | sim `periodic` | реестр `_register-all.js` ✅ |
|
||||||
|
| Модель атома (Бор) | sim `bohratom` | реестр ✅ |
|
||||||
|
| Орбитали | sim `orbitals` | реестр ✅ |
|
||||||
|
| Радиоактивность/изотопы | sim `radioactive` | реестр ✅ |
|
||||||
|
| Стехиометрия | sim `stoichiometry` | реестр ✅ |
|
||||||
|
| Титрование (нейтрализация) | sim `titration` | реестр ✅ |
|
||||||
|
| Качественный анализ | sim `qualanalysis` | реестр ✅ |
|
||||||
|
| Растворы / массовая доля | sim `solutions` | реестр ✅ |
|
||||||
|
| Песочница реакций | sim `chemsandbox` | реестр ✅ |
|
||||||
|
|
||||||
|
Монтаж: контейнер `<div id="sim-<id>"></div>` + `openSim('<id>')` (или прямой mount
|
||||||
|
через `window.LabRegistry`), как на остальных страницах.
|
||||||
|
|
||||||
|
### B. Общий хелпер `/js/chem8_svg.js` (по образцу `geom7_svg.js`, `alg10_svg.js`)
|
||||||
|
|
||||||
|
> **Рекомендация:** химические примитивы 8 и 9 классов сильно пересекаются. Реализовать
|
||||||
|
> файл так, чтобы его можно было продвинуть в **общий `/js/chem_svg.js`** (план Химии 9
|
||||||
|
> ссылается на `chem9_svg.js` — при совместной разработке свести в один shared-модуль и
|
||||||
|
> переиспользовать оба). Молекулы — **только через `biochem-core.js`**, не дублировать.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. Рендер уравнения реакции: коэффициенты, состояния (↑↓), стрелки, условия над стрелкой
|
||||||
|
const chemEq = (src, opts={}) => { /* токенизация формул, верхн./нижн. индексы, →/⇌/→[t°] */ };
|
||||||
|
|
||||||
|
// 2. Ион с зарядом: ionLabel('SO4', -2) → 'SO₄²⁻'
|
||||||
|
const ionLabel = (formula, charge) => { /* нижние индексы + надстрочный заряд */ };
|
||||||
|
|
||||||
|
// 3. Пробирка с осадком/газом/окраской (SVG-анимация)
|
||||||
|
const testTube = ({fill, precipitate, gas, color, label}) => { /* svg */ };
|
||||||
|
|
||||||
|
// 4. Треугольник n–m–M (звёздный виджет §6): кликаешь искомое → формула + калькулятор
|
||||||
|
const moleTriangle = (mount, {solveFor}) => { /* n=m/M, m=n·M, M=m/n */ };
|
||||||
|
|
||||||
|
// 5. Балансировщик уравнений (§8): подбор коэффициентов, проверка баланса атомов
|
||||||
|
const equationBalancer = (mount, {skeleton}) => { /* матрица атомов, подсветка дисбаланса */ };
|
||||||
|
|
||||||
|
// 6. Калькулятор степени окисления (§42): формула → с.о. каждого элемента (правила)
|
||||||
|
const oxStateCalc = (mount, {formula}) => { /* разбор, правила H+1/O−2/Σ=0 */ };
|
||||||
|
|
||||||
|
// 7. Балансировщик ОВР методом e-баланса (§44): полуреакции, НОК, коэффициенты
|
||||||
|
const redoxBalancer = (mount, {skeleton}) => { /* окислитель/восстановитель, Δe⁻ */ };
|
||||||
|
|
||||||
|
// 8. Орбитальная диаграмма (§33): orbitalDiagram('1s2 2s2 2p4') → клетки + ↑↓
|
||||||
|
const orbitalDiagram = (config) => { /* svg клетки, принцип Хунда/Паули */ };
|
||||||
|
|
||||||
|
// 9. Интерактивная таблица растворимости (§19,20,48): подсветка пары катион×анион (Р/Н/М/—)
|
||||||
|
const solubilityTable = (mount, {highlight}) => { /* из форзаца книги */ };
|
||||||
|
|
||||||
|
// 10. Интерактивный ряд активности металлов (§14,20): клик → предсказание реакции
|
||||||
|
const activitySeries = (mount, opts) => { /* K Ca Na Mg Al Zn Fe ... Au + (H₂) */ };
|
||||||
|
|
||||||
|
// 11. Мини-ПСХЭ с подсветкой (§1,26,34): элемент/группа/период/семейство
|
||||||
|
const miniPeriodic = (mount, {highlight, onClick}) => { /* интерактивная сетка */ };
|
||||||
|
|
||||||
|
// 12. Индикатор + шкала pH (§13,14,16,17): лакмус/фенолфталеин/метилоранж
|
||||||
|
const indicatorScale = (mount, {ph, indicator}) => { /* цвет полоски */ };
|
||||||
|
|
||||||
|
// 13. Анимация растворения/гидратации (§47): частицы воды окружают ионы/молекулы
|
||||||
|
const dissociationAnim = (mount, {substance}) => { /* canvas/SVG-частицы */ };
|
||||||
|
|
||||||
|
// 14. Классификатор-DnD (§10,13,16,19,46): drag формулы → класс/тип; проверка
|
||||||
|
const classifier = (mount, {items, buckets}) => { /* оксиды/кислоты/основания/соли/смеси */ };
|
||||||
|
|
||||||
|
// 15. Генетическая карта-граф (§22): Me/неMe → оксид → гидроксид → соль, клик-переходы
|
||||||
|
const geneticMap = (mount, opts) => { /* SVG-граф классов + рендер реакции перехода */ };
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Правила рендера химии (обязательны с §1)
|
||||||
|
|
||||||
|
1. **Формулы веществ** — нижние индексы для атомов ($H_2O$, $CaCO_3$), верхние для зарядов
|
||||||
|
ионов ($SO_4^{2-}$) и степеней окисления ($\overset{+2}{Ca}$); единый рендер через
|
||||||
|
`chemEq`/`ionLabel`, не «сырой» текст.
|
||||||
|
2. **Уравнения реакций** — всегда сбалансированы; стрелки `=`/`→` (необратимая),
|
||||||
|
`⇌` (обратимая), `↑` (газ), `↓` (осадок), условия над стрелкой ($t$, кат., эл.ток).
|
||||||
|
3. **Состояние/признак** — для качественных реакций показывать цвет осадка, пузырьки газа,
|
||||||
|
изменение окраски индикатора (через `testTube`/`indicatorScale`).
|
||||||
|
4. **Количественные расчёты** — каждый расчётный § даёт калькулятор/тренажёр с пошаговым
|
||||||
|
решением (дано → формула → подстановка → ответ с единицами), не только готовый ответ.
|
||||||
|
5. **Молекулярные модели** — структурная формула + 3D (biochem-core) для каждой изучаемой
|
||||||
|
молекулы (§37–38, §41); для типов решёток — 3D-ячейки.
|
||||||
|
6. **Цвета — химически достоверные**: осадки ($Cu(OH)_2$ голубой, $Fe(OH)_3$ бурый,
|
||||||
|
$Zn(OH)_2$ белый); индикаторы (фенолфталеин в щёлочи малиновый, лакмус в кислоте красный,
|
||||||
|
метилоранж в кислоте розовый).
|
||||||
|
7. **Безопасность** — где уместно (растворение кислот/щелочей, разбавление) — заметка-«скрепка».
|
||||||
|
8. **KaTeX-эскейпы** — в JS-шаблонах двойной backslash (`\\to`, `\\downarrow`, `\\rightleftharpoons`).
|
||||||
|
9. **Drag/слайдеры** — `window`-listeners + `{passive:false}` + state ВЫШЕ `redraw()`
|
||||||
|
(стандарт геометрии), `touch-action:none` на draggable SVG/canvas, **без `setPointerCapture`**.
|
||||||
|
10. **Без эмоджи** — только inline SVG `.ic`/`.ico` (правило проекта [[feedback_no_emoji]]).
|
||||||
|
|
||||||
|
### D. Типы интерактивов по темам 8 класса
|
||||||
|
|
||||||
|
| Тип темы | Интерактив |
|
||||||
|
|----------|------------|
|
||||||
|
| Количество вещества (§3–7, 9) | `moleTriangle`, калькуляторы $M/V_m/N$, sim `stoichiometry` |
|
||||||
|
| Химические реакции (§8) | `equationBalancer`, классификатор типов |
|
||||||
|
| Классы соединений (§10–21) | `classifier`, матрицы реакций, `indicatorScale`, `solubilityTable`, `activitySeries` |
|
||||||
|
| Генетическая связь (§22) | `geneticMap` |
|
||||||
|
| Периодический закон / ПСХЭ (§24–28, 34) | `miniPeriodic`, sim `periodic`, графики трендов |
|
||||||
|
| Строение атома (§29–33) | sim `bohratom`, sim `orbitals`, sim `radioactive`, `orbitalDiagram` |
|
||||||
|
| Химическая связь (§36–41) | slider ЭО, biochem-core 3D, ионная/металлич. решётка, 4 типа кристаллов |
|
||||||
|
| ОВР (§42–44) | `oxStateCalc`, `redoxBalancer`, визуализация переноса $e^-$ |
|
||||||
|
| Растворы (§46–52) | `dissociationAnim`, график растворимости, sim `solutions`, калькулятор $c$ |
|
||||||
|
| Качественные/амфотерность (§18,25) | `testTube` + уравнение + признак |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 СТРУКТУРА КАЖДОГО § (стандарт наполнения)
|
||||||
|
|
||||||
|
**Теория (3–4 карточки):**
|
||||||
|
- `theory` — основное определение/понятие + наглядная SVG/модель
|
||||||
|
- `rule` — ключевая закономерность/формула (рамка)
|
||||||
|
- `example` — разобранный пример (реакция/расчёт) с пошаговым рендером
|
||||||
|
- (для прикладных §) `apply` — применение/значение (инфографика)
|
||||||
|
|
||||||
|
**Интерактивы (4–6 на §):**
|
||||||
|
1. **Звёздный виджет** темы (из карты содержания)
|
||||||
|
2. **Конструктор/симулятор** (slider / drag / sim из реестра)
|
||||||
|
3. **Калькулятор** ($M$, $n$, $w$, $c$, с.о., по уравнению) — где применимо
|
||||||
|
4. **DnD-классификатор** (классы веществ, тип связи/реакции, тип решётки)
|
||||||
|
5. **Тренажёр** — 5 задач с inline-наглядностью (формула/уравнение/модель в условии)
|
||||||
|
6. **Босс §** — 4 интеграционные задачи (+5 XP каждая)
|
||||||
|
|
||||||
|
**Дополнительно:** пополнение глоссария (термины §, `[[ссылки]]`), «Вопросы и задания»
|
||||||
|
из учебника (адаптированные, с проверкой), проходящий jsdom-тест страницы.
|
||||||
|
|
||||||
|
**Финал главы:** итоговая шпаргалка (mini-cards), карта связей (SVG-граф понятий),
|
||||||
|
7 интегрированных боссов (+10 XP), achievement «Мастер главы N» (+50 XP, confetti),
|
||||||
|
кнопка перехода к следующей главе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 ПОРЯДОК РЕАЛИЗАЦИИ (по фазам)
|
||||||
|
|
||||||
|
### Phase 0 — Фундамент (hub + каркасы глав)
|
||||||
|
- **`chemistry_8_hub.html`** — хаб-каталог 7 глав по образцу `physics_9_hub.html`: палитра
|
||||||
|
**amber**, водяной знак «ХИМИЯ», карточки глав с прогрессом (грузится из
|
||||||
|
`/api/textbooks/chemistry-8/children`), блок «Финал курса» (шпаргалка + боссы — наполняется в Phase 7),
|
||||||
|
achievement-strip «Химик 8 класса», тема (localStorage `chemistry8_theme`).
|
||||||
|
- **7 файлов глав** `chemistry_8_intro.html` + `chemistry_8_ch1..ch6.html` — на Phase 0
|
||||||
|
валидные каркасы-заглушки (header с водяным знаком, hero, sidebar-оглавление §, контейнер
|
||||||
|
параграфов, XP/tracker-интеграция), наполнение § — в Phase 1–6.
|
||||||
|
- **`/js/chem8_svg.js`** (хелперы B — заглушки → реализация по фазам).
|
||||||
|
- Подключить `biochem-core.js` + нужные симуляторы на страницах глав.
|
||||||
|
- **Миграция `041_chemistry8_hub.sql`** (следующий номер после `040_content_access.sql`):
|
||||||
|
**INSERT** родителя `chemistry-8` (`html_path='chemistry_8_hub.html'`, `para_count=52`,
|
||||||
|
`color='amber'`, `parent_slug=NULL`) + **7 детей** `chemistry-8-intro`/`-ch1..-ch6`
|
||||||
|
(`parent_slug='chemistry-8'`, свои `html_path`/`para_count`/`color`/`sort_order`) — по образцу
|
||||||
|
`038_physics_9_hub.sql`. Применить `npm run migrate`.
|
||||||
|
- jsdom-тест-каркас: хаб строится, все 8 файлов парсятся, ссылки глав ведут на существующие slug.
|
||||||
|
|
||||||
|
### Phase 1 — Вводный раздел «Количественные понятия» (§§1–9) + ПР1 — фундамент расчётов
|
||||||
|
Базовые движки: калькулятор $M_r$ (biochem-core), `moleTriangle`, связка $m$–$n$–$V$–$N$,
|
||||||
|
`equationBalancer`, sim `stoichiometry`. **Критично** — эти расчёты используются во всех главах.
|
||||||
|
|
||||||
|
### Phase 2 — Глава 1 «Классы неорганических соединений» (§§10–23) + Лаб.1,2 + ПР2,3
|
||||||
|
Самая объёмная. Закладываем `classifier`, `indicatorScale`, `solubilityTable`,
|
||||||
|
`activitySeries`, матрицы реакций, `testTube` (первые качественные/нейтрализация),
|
||||||
|
`geneticMap` (§22), sim `titration`.
|
||||||
|
|
||||||
|
### Phase 3 — Глава 2 «Периодический закон и ПСХЭ» (§§24–28) + Лаб.3
|
||||||
|
`miniPeriodic`, sim `periodic`, амфотерность ($Zn(OH)_2$ — обе реакции + `testTube`),
|
||||||
|
демонстрация периодичности.
|
||||||
|
|
||||||
|
### Phase 4 — Глава 3 «Строение атома» (§§29–35)
|
||||||
|
sim `bohratom`, sim `orbitals`, sim `radioactive`, `orbitalDiagram`, графики периодических
|
||||||
|
трендов, генератор «паспорта элемента».
|
||||||
|
|
||||||
|
### Phase 5 — Глава 4 «Химическая связь» (§§36–41) + Лаб.4
|
||||||
|
slider ЭО → тип связи, biochem-core 3D-модели, ионная/металлическая решётки,
|
||||||
|
4 типа кристаллических решёток (3D) и связь «тип → свойства».
|
||||||
|
|
||||||
|
### Phase 6 — Глава 5 «ОВР» (§§42–45) + Глава 6 «Растворы» (§§46–52) + ПР4
|
||||||
|
`oxStateCalc`, `redoxBalancer`, визуализация переноса $e^-$; затем `dissociationAnim`,
|
||||||
|
график растворимости, sim `solutions`, калькулятор $c$, ПР4.
|
||||||
|
|
||||||
|
### Phase 7 — Финалы глав + общий финал учебника
|
||||||
|
Шпаргалки и карты связей по каждой главе; интегрированные боссы + achievements;
|
||||||
|
**большой финал**: генетическая карта классов + строение/связь, итоговый босс-квест,
|
||||||
|
ачивка «Химик 8 класса»; глоссарий собран и связан `[[ссылками]]`.
|
||||||
|
|
||||||
|
### Phase 8 — Качество и админка
|
||||||
|
Полный прогон jsdom-тестов (каждый § — builder не stub); аудит баланса уравнений и
|
||||||
|
KaTeX/`chemEq`-эскейпов; синхронизация с админкой (если новые sim в `lab.html` →
|
||||||
|
обновить `ADMIN_SIMS` в `admin.html` — [[feedback_sims_admin_sync]]); проверка доступа
|
||||||
|
по классам/ученикам ([[project_content_access]], `/api/access`).
|
||||||
|
|
||||||
|
> Рекомендуемый темп: внутри фазы — по 2–3 § за «волну», каждая волна = commit +
|
||||||
|
> проходящий jsdom-тест (правило CLAUDE.md: commit изменённых файлов + push).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ ИНТЕГРАЦИЯ С ПРОЕКТОМ
|
||||||
|
|
||||||
|
| Точка | Действие |
|
||||||
|
|-------|----------|
|
||||||
|
| **БД каталог** | `chemistry-8` в `textbooks` **отсутствует** → миграция `041_chemistry8_hub.sql`: INSERT родитель + 7 детей (образец — `038_physics_9_hub.sql`). Каталог `/api/textbooks` показывает только `parent_slug IS NULL`; хаб тянет детей через `/api/textbooks/chemistry-8/children`. |
|
||||||
|
| **Прогресс/XP** | Автоматически: `textbook-xp-widget.js` (+5 XP/§), `textbook-tracker.js`, `LS.xp`. Доп. XP за боссов — по образцу `phys7_ch1_widgets.js`. |
|
||||||
|
| **Симуляторы** | Реестр `frontend/js/labs/_register-all.js`. Нужные химические sim уже зарегистрированы: `periodic`, `bohratom`, `orbitals`, `radioactive`, `stoichiometry`, `titration`, `qualanalysis`, `solutions`, `chemsandbox`. |
|
||||||
|
| **Молекулы** | `biochem-core.js` (парсинг, $M$, 2D/3D, VSEPR). |
|
||||||
|
| **Бэкенд** | Роуты готовы: `backend/src/routes/textbooks.js` (catalog/progress/bookmarks). Доступ: `backend/src/services/contentAccess.js`. |
|
||||||
|
| **Глоссарий** | Виджет всплывающих определений на странице (общего нет — реализовать). |
|
||||||
|
| **Тесты** | `cd backend && npm test` (jsdom). На каждый § — тест: страница строится, builder не stub, уравнения сбалансированы. |
|
||||||
|
| **Админка** | Новые sim в `lab.html` → синхронно `ADMIN_SIMS` в `admin.html`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ КРИТИЧЕСКИЕ ПРАВИЛА
|
||||||
|
|
||||||
|
### ❌ НЕ делать
|
||||||
|
- «Сырые» формулы текстом — только `chemEq`/`ionLabel`/KaTeX.
|
||||||
|
- Несбалансированные уравнения (аудит баланса перед commit).
|
||||||
|
- Дублировать молекулярный движок — использовать `biochem-core.js`.
|
||||||
|
- `setPointerCapture` (теряется после `innerHTML`-replace) → `window`-listeners + state-flag.
|
||||||
|
- `\to`, `\downarrow`, `\rightleftharpoons` без удвоения backslash в JS-шаблонах.
|
||||||
|
- Slider-диапазоны за пределы химически возможного (концентрации, температуры, $V_m$).
|
||||||
|
- Эмоджи — запрещены; только inline SVG `.ic`.
|
||||||
|
- **Grep tool — запрещён**; поиск только `ast-index` ([[reference_sqlite_node]] и правила проекта).
|
||||||
|
|
||||||
|
### ✅ Обязательно
|
||||||
|
- Каждый commit → jsdom-тест 100% pass.
|
||||||
|
- Аудит баланса уравнений + KaTeX-эскейпов после каждой волны.
|
||||||
|
- Расчётный § = калькулятор/тренажёр с **пошаговым** решением и единицами измерения.
|
||||||
|
- Качественная реакция = уравнение (молек.+, где есть, ионное) **+ видимый признак**.
|
||||||
|
- Цвета осадков/индикаторов — химически достоверные.
|
||||||
|
- Все builder-функции в конце финальной волны главы — НЕ stub'ы.
|
||||||
|
- Коммитить только изменённые файлы (не `git add -A`), сразу push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Оценка объёма
|
||||||
|
|
||||||
|
| Раздел | § | Лаб/ПР | Ожидаемый LOC |
|
||||||
|
|--------|---|--------|---------------|
|
||||||
|
| Вводный (кол-во вещества) | 9 | ПР1 | ~7 000 (+`moleTriangle`, `equationBalancer`, `stoichiometry`) |
|
||||||
|
| Гл.1 Классы соединений | 14 | Лаб1,2 + ПР2,3 | ~16 000 (+`classifier`, `solubilityTable`, `activitySeries`, `geneticMap`) |
|
||||||
|
| Гл.2 ПЗ и ПСХЭ | 5 | Лаб3 | ~5 000 (+`miniPeriodic`, амфотерность) |
|
||||||
|
| Гл.3 Строение атома | 7 | — | ~7 000 (+`orbitalDiagram`, sim bohratom/orbitals/radioactive) |
|
||||||
|
| Гл.4 Химическая связь | 6 | Лаб4 | ~6 500 (+3D-модели, 4 решётки) |
|
||||||
|
| Гл.5 ОВР | 4 | — | ~4 500 (+`oxStateCalc`, `redoxBalancer`) |
|
||||||
|
| Гл.6 Растворы | 7 | ПР4 | ~7 000 (+`dissociationAnim`, sim solutions, калькулятор $c$) |
|
||||||
|
| Финалы глав + общий | — | — | ~5 000 |
|
||||||
|
| `/js/chem8_svg.js` хелперы | — | — | ~3 000 |
|
||||||
|
| Хаб + 7 каркасов глав (Phase 0) | — | — | ~3 000 |
|
||||||
|
| **Итого** | **52** | **4 лаб + 4 ПР** | **~64 000 LOC** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 Запуск
|
||||||
|
|
||||||
|
**Phase 0**: `chemistry_8_hub.html` (по образцу `physics_9_hub.html`) + 7 каркасов глав
|
||||||
|
(`chemistry_8_intro.html`, `chemistry_8_ch1..ch6.html`) + `/js/chem8_svg.js` (скелет) +
|
||||||
|
подключение `biochem-core.js`/симуляторов + миграция `041_chemistry8_hub.sql` (родитель + 7 детей)
|
||||||
|
+ `npm run migrate` + jsdom-каркас.
|
||||||
|
**Phase 1**: Вводный раздел (§§1–9) — закладываем движки расчётов (`moleTriangle`,
|
||||||
|
`equationBalancer`, sim `stoichiometry`), от которых зависят все главы.
|
||||||
|
|
||||||
|
Дальше — последовательно по главам (Phase 2 → 6), затем финалы (Phase 7) и качество (Phase 8).
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# План апгрейда: Химия 8 — больше интерактива и наполнения
|
||||||
|
|
||||||
|
> База готова: вводный раздел + 6 глав, все 52 §, движок `chem8_engine.js` + 12 виджетов,
|
||||||
|
> 37 тестов. Этот план — **следующий уровень**: финал курса, глоссарий, новые движки-виджеты,
|
||||||
|
> 3D-модели молекул, обогащение контента и финалов глав, синхронизация с админкой.
|
||||||
|
|
||||||
|
Принципы (как в базовом плане): эталонная SPA-структура, без эмоджи (только inline SVG `.ic`),
|
||||||
|
KaTeX-эскейпы, jsdom-проверка каждого нового виджета, поиск через `ast-index`, изоляция
|
||||||
|
химии на ветке `feature/chemistry-8` (cherry-pick из рабочей ветки).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## U1 — Финал курса в хабе (Phase 7) ⭐ старт
|
||||||
|
|
||||||
|
`chemistry_8_hub.html` сейчас содержит заглушку «Финал курса появится позже». Заменить на
|
||||||
|
полноценный финал по образцу `physics_9_hub.html`:
|
||||||
|
|
||||||
|
- **Шпаргалка курса** — 7 cheat-cards (вводный + 6 глав) с ключевыми формулами/реакциями.
|
||||||
|
- **10 интегрированных боссов** — задачи, каждая связывает ≥2 раздела (например, «масса осадка
|
||||||
|
по уравнению РИО», «c раствора + расчёт по уравнению»). +15 XP за босса.
|
||||||
|
- **Ачивка «Химик 8 класса»** — при всех 10 → +150 XP, confetti, CTA «К каталогу».
|
||||||
|
- Прогресс-бар боссов, lazy-render при раскрытии аккордеона, localStorage
|
||||||
|
(`chemistry8_course_bosses`, `chemistry8_course_master`).
|
||||||
|
- jsdom-тест: финал раскрывается, 10 боссов рендерятся, KaTeX, без ошибок.
|
||||||
|
|
||||||
|
## U2 — Глоссарий (Phase 8a)
|
||||||
|
|
||||||
|
Единый виджет всплывающих определений терминов на всех 8 страницах:
|
||||||
|
|
||||||
|
- `chem8_glossary.js` — словарь ~120 терминов (оксид, кислота, основание, соль, моль, валентность,
|
||||||
|
степень окисления, электроотрицательность, изотоп, орбиталь, растворимость, концентрация …).
|
||||||
|
- Авто-подсветка терминов в тексте `.card-body` (`<abbr class="gloss" data-term="…">`) +
|
||||||
|
popover с определением и `[[ссылками]]` на связанные термины.
|
||||||
|
- Кнопка «Глоссарий» в header каждой главы → модальное окно со списком/поиском.
|
||||||
|
- Тест: словарь парсится, термин даёт определение.
|
||||||
|
|
||||||
|
## U3 — Новые движки-виджеты (chem8_svg.js)
|
||||||
|
|
||||||
|
Заменить оставшиеся заглушки реальными реализациями + добавить новые:
|
||||||
|
|
||||||
|
| Виджет | § | Что делает |
|
||||||
|
|--------|---|------------|
|
||||||
|
| `dissociationAnim` | §47, ТЭД | анимация распада соли/кислоты на ионы в воде (canvas/SVG-частицы) |
|
||||||
|
| `geneticMap` | §22 | интерактивный граф классов (Me→оксид→основание→соль), клик по ребру → реакция |
|
||||||
|
| `redoxBalancer` | §44 | общий балансировщик ОВР методом e-баланса (не преднабор) |
|
||||||
|
| `reactionMatrix` | §11,14,17,20 | матрица «реагент × реагент» → продукт/нет реакции |
|
||||||
|
| `phScale` | §13,16 | расширенная шкала pH с примерами бытовых веществ |
|
||||||
|
| `ionConverter` | §9,РИО | молекулярное → полное ионное → сокращённое ионное уравнение |
|
||||||
|
|
||||||
|
Каждый — с jsdom-смоук-тестом монтажа и расчёта.
|
||||||
|
|
||||||
|
## U4 — 3D-модели молекул (biochem-core)
|
||||||
|
|
||||||
|
Интегрировать `biochem-core.js` (window.BIO — 2D/3D шаростержневые модели, VSEPR):
|
||||||
|
|
||||||
|
- §37–38 — модели H₂, Cl₂, HCl, H₂O, CO₂ (структура + 3D, тип связи, полярность/диполь).
|
||||||
|
- §41 — 3D-ячейки 4 типов решёток.
|
||||||
|
- Хелпер `chem8Mol(mount, formula)` — обёртка над BIO для монтажа модели по формуле.
|
||||||
|
- Тест: модель строится, молярная масса совпадает с `Chem8.molarMass`.
|
||||||
|
|
||||||
|
## U5 — Обогащение контента §
|
||||||
|
|
||||||
|
По канве учебников Исаченковой (см. [[reference_textbook_sources]]):
|
||||||
|
|
||||||
|
- **8–10 задач** на § (сейчас 3–5): добавить уровни сложности, задачи «для любознательных».
|
||||||
|
- **life-grid** примеры из жизни в каждый § (где уместно).
|
||||||
|
- **insight-box** «это интересно» / историческая справка.
|
||||||
|
- **«Контрольные вопросы»** из учебника (адаптированные) — уже частично есть, расширить.
|
||||||
|
- Разобранные **примеры с пошаговым решением** (`exa-step`) в расчётных §.
|
||||||
|
|
||||||
|
## U6 — Финалы глав (интегрированные боссы)
|
||||||
|
|
||||||
|
Сейчас финал главы = шпаргалка + POOLS-задачи. Усилить:
|
||||||
|
|
||||||
|
- Каждый финал главы → **карта связей** (SVG-граф понятий главы).
|
||||||
|
- **Achievement-strip** «Мастер главы N» (+50 XP, confetti) при полном прохождении.
|
||||||
|
- Кнопка перехода к следующей главе.
|
||||||
|
|
||||||
|
## U7 — Синхронизация с админкой и доступом (Phase 8b)
|
||||||
|
|
||||||
|
- Проверить, что `chemistry-8` и 7 детей видны в админке (`/api/textbooks/admin/all`).
|
||||||
|
- Если добавлялись sim в `lab.html` → обновить `ADMIN_SIMS` в `admin.html` ([[feedback_sims_admin_sync]]).
|
||||||
|
- Доступ по классам/ученикам ([[project_content_access]], `/api/access`) — проверить выдачу.
|
||||||
|
- Прогресс/XP агрегируется в хабе (`/api/textbooks/chemistry-8/children`) — проверить.
|
||||||
|
|
||||||
|
## U8 — Качество
|
||||||
|
|
||||||
|
- jsdom-смоук на каждый новый виджет (монтаж + расчёт).
|
||||||
|
- Аудит баланса всех уравнений и KaTeX/`chemEq`-эскейпов.
|
||||||
|
- Полный прогон `cd backend && npm test`.
|
||||||
|
- Аудит доступности (контраст, фокус, клавиатура для боссов/тренажёров).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок выполнения
|
||||||
|
|
||||||
|
**U1 (Phase 7)** → **U2 глоссарий** → **U3 виджеты** → **U4 3D** → **U5 контент** →
|
||||||
|
**U6 финалы глав** → **U7 админка** → **U8 качество**.
|
||||||
|
|
||||||
|
Темп: один U-блок = волна = commit + проходящие тесты + cherry-pick на `feature/chemistry-8`.
|
||||||
|
|
||||||
|
**Старт: U1 — финал курса в хабе.**
|
||||||
Reference in New Issue
Block a user