feat: add interactive geo drag models + chapter 1 questions + car path animation

This commit is contained in:
hangshuo652
2026-06-29 09:27:06 +08:00
commit 2359ac6556
5 changed files with 2718 additions and 0 deletions
+2272
View File
@@ -0,0 +1,2272 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<title>MV1 赛道数学</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;700;900&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Noto Sans SC','Inter',sans-serif;
background:#05050a;
min-height:100vh;
color:#c8c8d0;
overflow-x:hidden;
}
.app {
max-width:820px; margin:0 auto; padding:20px;
position:relative;
}
/* === HUD === */
.hud {
background:rgba(8,8,20,0.85); border:1px solid rgba(225,6,0,0.12);
padding:10px 18px; display:flex; align-items:center; gap:12px; margin-bottom:18px;
backdrop-filter:blur(8px); border-radius:4px;
}
.hud .logo { font-weight:900; font-size:15px; letter-spacing:2px; }
.hud .logo .r { color:#e10600; }
.hud .logo .y { color:#ffd700; }
.hud .logo .w { color:rgba(255,255,255,0.85); }
.hud-spacer { flex:1; }
.hud-stat { text-align:center; padding:0 10px; border-right:1px solid rgba(255,255,255,0.03); }
.hud-stat:last-child { border-right:none; }
.hud-stat .lbl { font-size:8px; color:rgba(255,255,255,0.4); letter-spacing:2px; font-weight:700; }
.hud-stat .val { font-size:18px; font-weight:900; font-family:'Inter',monospace; }
.hud-stat .val.r { color:#e10600; }
.hud-stat .val.y { color:#ffd700; }
.mbtn {
width:28px; height:28px; border:1px solid rgba(255,255,255,0.08); background:transparent;
color:rgba(255,255,255,0.4); font-size:12px; cursor:pointer; transition:all 0.3s;
display:flex; align-items:center; justify-content:center; flex-shrink:0; border-radius:2px;
}
.mbtn:hover { border-color:rgba(255,255,255,0.15); color:rgba(255,255,255,0.7); }
.mbtn.on { color:#ffd700; border-color:rgba(255,215,0,0.2); background:rgba(255,215,0,0.04); }
/* === pages === */
.pg { display:none; }
.pg.act { display:block; animation:fIn 0.35s ease; }
@keyframes fIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-6px)} 40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(4px)} }
@keyframes pop { 0%{opacity:0;transform:scale(0.8) translateY(16px)} 100%{opacity:1;transform:scale(1) translateY(0)} }
@keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.05)} }
@keyframes slideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
@keyframes glow { 0%,100%{box-shadow:0 0 4px rgba(255,215,0,0.1)} 50%{box-shadow:0 0 16px rgba(255,215,0,0.3)} }
@keyframes starPop { 0%{opacity:0;transform:scale(0) rotate(-180deg)} 60%{transform:scale(1.3) rotate(10deg)} 100%{opacity:1;transform:scale(1) rotate(0deg)} }
@keyframes bounceIn { 0%{opacity:0;transform:scale(0.3)} 50%{transform:scale(1.08)} 70%{transform:scale(0.95)} 100%{opacity:1;transform:scale(1)} }
/* ===== TRACK MAP ===== */
.track-wrap {
position:relative; width:100%; aspect-ratio:1/1.5; max-height:620px;
margin:0 auto 12px;
}
.track-wrap svg { width:100%; height:100%; display:block; }
.track-road { fill:none; stroke:rgba(255,255,255,0.03); stroke-width:16; stroke-linecap:round; }
.track-bg { fill:none; stroke:rgba(255,255,255,0.08); stroke-width:3; stroke-linecap:round; }
.track-fill { fill:none; stroke:rgba(255,255,255,0.25); stroke-width:1.5; stroke-dasharray:6,8; stroke-linecap:round; }
.track-done { fill:none; stroke:#ffd700; stroke-width:3; stroke-linecap:round; filter:drop-shadow(0 0 10px rgba(255,215,0,0.3)); }
.cp {
cursor:pointer; transition:all 0.3s;
}
.cp circle:first-of-type { pointer-events:all; cursor:pointer; }
.cp:hover circle { r:20; }
.cp.locked { opacity:0.25; }
.cp.locked circle:last-of-type { stroke:rgba(255,255,255,0.06); fill:rgba(255,255,255,0.02); }
.cp.locked + text { fill:rgba(255,255,255,0.06); }
.cp.lock-icon { font-size:10px; fill:rgba(255,255,255,0.25); text-anchor:middle; font-family:'Inter',monospace; }
.cp.done circle { fill:#ffd700; stroke:#ffd700; }
.cp-label {
font-size:9px; font-weight:900; fill:rgba(255,255,255,0.65); letter-spacing:1px;
transition:all 0.3s; text-anchor:middle; font-family:'Inter',monospace;
}
.cp.done .cp-label { fill:#ffd700; }
.cp:not(.locked):not(.done):hover .cp-label { fill:#fff; }
.cp-stars { font-size:9px; text-anchor:middle; fill:rgba(255,255,255,0.5); }
.cp-name {
font-size:10px; font-weight:700; fill:rgba(255,255,255,0.35);
text-anchor:middle; letter-spacing:1px;
}
/* F1 car on track */
.track-car {
position:absolute; width:66px; height:30px;
pointer-events:none;
filter:drop-shadow(0 0 10px rgba(225,6,0,0.35));
z-index:10;
}
.track-car .ci { width:100%; height:100%; object-fit:contain; display:block; }
.track-car .cs { width:100%; height:100%; display:block; overflow:visible; }
.track-car .cs.hide { display:none; }
.track-car.moving {
filter:drop-shadow(0 0 18px rgba(255,215,0,0.5));
}
.mv1-photo {
width:80px; height:80px; border-radius:50%; object-fit:cover;
border:2px solid #ffd700; margin:0 auto 8px; display:block;
box-shadow:0 0 20px rgba(255,215,0,0.1);
}
.mv1-photo.hide { display:none; }
.mv1-av {
width:80px; height:80px; border-radius:50%; margin:0 auto 8px;
background:linear-gradient(135deg,#e10600,#aa0000);
display:flex; align-items:center; justify-content:center;
font-size:28px; font-weight:900; color:#fff; font-family:'Inter',monospace;
border:2px solid rgba(255,215,0,0.2);
}
.mv1-av.hide { display:none; }
/* Start/Finish line */
.sf { fill:#ffd700; opacity:0.15; }
/* === Chapter view === */
.ch { margin-bottom:16px; }
.ch .gl { font-size:12px; font-weight:700; color:#e10600; letter-spacing:2px; font-family:'Inter',monospace; }
.ch h2 { font-size:24px; font-weight:900; color:#fff; margin:2px 0; }
.ch .sm { font-size:13px; color:rgba(255,255,255,0.45); letter-spacing:1px; font-weight:700; }
/* === Buttons === */
.bk {
background:rgba(8,8,20,0.6); border:1px solid rgba(255,255,255,0.06);
color:rgba(255,255,255,0.6); padding:9px 16px; cursor:pointer; font-size:14px; font-family:inherit;
transition:all 0.25s; margin-bottom:16px; font-weight:700; letter-spacing:1px; border-radius:3px;
}
.bk:hover { background:rgba(8,8,20,0.6); color:rgba(255,255,255,0.8); border-color:rgba(225,6,0,0.2); }
.act {
display:none; margin-top:10px; padding:10px 28px; border:none;
font-size:14px; font-weight:700; font-family:'Inter',monospace; cursor:pointer; letter-spacing:1px;
background:#e10600; color:#fff; transition:all 0.3s; border-radius:3px;
}
.act.show { display:inline-block; }
.act:hover { background:#aa0000; transform:translateY(-1px); box-shadow:0 4px 12px rgba(225,6,0,0.2); }
.act:active { transform:translateY(0); }
/* === Panels === */
.pn {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
padding:20px; margin-bottom:8px; border-radius:2px;
}
.pn h3 { font-size:18px; font-weight:900; color:#fff; margin-bottom:10px; letter-spacing:-0.3px; }
.kpTitle { font-size:20px; font-weight:900; color:#ffd700; margin-bottom:14px; letter-spacing:0; border-bottom:1px solid rgba(255,215,0,0.08); padding-bottom:10px; animation:slideUp 0.4s ease; }
.pn p { font-size:15px; line-height:2; color:rgba(255,255,255,0.7); }
.co {
background:linear-gradient(135deg,rgba(225,6,0,0.03),rgba(8,8,20,0.6));
border-left:3px solid #e10600; padding:12px 16px; margin:12px 0; position:relative;
}
.co::before {
content:'PACE NOTE'; position:absolute; top:-7px; right:8px;
font-size:7px; font-weight:900; color:rgba(225,6,0,0.35); letter-spacing:2px;
}
.co p { color:rgba(255,255,255,0.75); font-size:15px; line-height:1.8; }
.co strong { color:#ffd700; }
.qp {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #ffd700; padding:20px; margin-bottom:8px; border-radius:2px;
}
.qp .qn { font-size:11px; color:#ffd700; font-weight:700; letter-spacing:2px; margin-bottom:6px; font-family:'Inter',monospace; }
.qp .qt { font-size:18px; font-weight:700; color:#fff; margin-bottom:16px; line-height:1.7; }
.qo { display:flex; flex-direction:column; gap:5px; }
.qo .opt {
padding:12px 16px; border:1px solid rgba(255,255,255,0.04);
cursor:pointer; font-size:15px; font-family:inherit; transition:all 0.2s;
background:rgba(255,255,255,0.02); text-align:left; color:rgba(255,255,255,0.75);
display:flex; align-items:center; gap:10px;
border-left:3px solid transparent; border-radius:3px;
}
.qo .opt .lb {
width:28px; height:28px; display:flex; align-items:center; justify-content:center;
font-weight:900; font-size:13px; background:rgba(255,255,255,0.04); color:rgba(255,255,255,0.3);
flex-shrink:0; font-family:'Inter',monospace; border-radius:3px;
}
.qo .opt:hover { border-color:rgba(255,215,0,0.08); background:rgba(255,215,0,0.02); border-left-color:rgba(255,215,0,0.2); }
.qo .opt:hover .lb { background:rgba(255,215,0,0.08); color:#ffd700; }
.qo .opt.fail { border-color:rgba(225,6,0,0.1); background:rgba(225,6,0,0.03); border-left-color:#e10600; animation:shake 0.4s ease; }
.qo .opt.fail .lb { background:#e10600; color:#fff; }
.qo .opt.pass { border-color:rgba(46,204,113,0.1); background:rgba(46,204,113,0.03); border-left-color:#2ecc71; animation:pulse 0.3s ease; }
.qo .opt.pass .lb { background:#2ecc71; color:#fff; }
.qo .opt.disabled { pointer-events:none; opacity:0.3; }
.qfb { padding:14px 18px; margin-top:10px; font-size:15px; font-weight:700; display:none; line-height:1.8; }
.qfb.show { display:block; }
.qfb.pass { background:rgba(46,204,113,0.05); color:#2ecc71; border:1px solid rgba(46,204,113,0.06); border-left:3px solid #2ecc71; }
.qfb.fail { background:rgba(225,6,0,0.05); color:rgba(255,255,255,0.85); border:1px solid rgba(225,6,0,0.06); border-left:3px solid #e10600; }
.rv {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid rgba(255,215,0,0.15); padding:20px; margin-bottom:8px; text-align:center; border-radius:2px;
}
.rv h3 { color:#fff; margin-bottom:16px; font-size:14px; font-weight:700; }
.rline {
position:relative; height:3px;
background:repeating-linear-gradient(90deg,#e10600 0,#e10600 6px,transparent 6px,transparent 12px);
margin:28px 10px 12px;
}
.rline .mk { position:absolute; top:-6px; transform:translateX(-50%); font-size:10px; font-weight:700; color:rgba(255,255,255,0.1); font-family:'Inter',monospace; }
.rline .mk.z { color:#ffd700; }
.rline .mk::before { content:''; position:absolute; top:14px; left:50%; width:1px; height:5px; background:rgba(255,255,255,0.04); }
.car { position:absolute; font-size:20px; top:-24px; transform:translateX(-50%); transition:left 0.5s cubic-bezier(.34,1.56,.64,1); }
.car .vl { position:absolute; top:-14px; left:50%; transform:translateX(-50%); font-size:9px; font-weight:700; white-space:nowrap; color:#fff; background:rgba(225,6,0,0.5); padding:1px 5px; font-family:'Inter',monospace; }
/* ===== PODIUM ===== */
.po {
display:none; position:fixed; inset:0; background:rgba(5,5,10,0.88); backdrop-filter:blur(14px);
z-index:100; align-items:center; justify-content:center;
}
.po.show { display:flex; }
.pm {
background:#0a0a1a; border:1px solid rgba(225,6,0,0.1); border-radius:4px;
padding:40px 32px 28px; text-align:center; max-width:360px; width:90%;
animation:pop 0.5s cubic-bezier(.34,1.56,.64,1);
position:relative; overflow:hidden;
}
.pm::before {
content:''; position:absolute; top:0; left:0; right:0; height:2px;
background:linear-gradient(90deg,#e10600,#ffd700,#e10600);
}
.pm::after {
content:'\u{1F3C6}'; position:absolute; top:-20px; right:-20px; font-size:100px;
opacity:0.03; transform:rotate(15deg);
}
@keyframes pop { from{opacity:0;transform:scale(0.9) translateY(16px)} to{opacity:1;transform:scale(1) translateY(0)} }
.pm .pn1 { font-size:56px; font-weight:900; font-family:'Inter',monospace; color:#ffd700; line-height:1; text-shadow:0 0 30px rgba(255,215,0,0.15); }
.pm .pcup { font-size:50px; display:block; margin:2px 0; }
.pm h2 { font-size:24px; font-weight:900; color:#fff; letter-spacing:1px; }
.pm .ps { font-size:18px; margin:8px 0 4px; letter-spacing:5px; }
.pm .pxp { color:#ffd700; font-weight:900; font-size:16px; margin:2px 0 6px; font-family:'Inter',monospace; }
.pm .pms { color:rgba(255,255,255,0.4); font-size:12px; letter-spacing:1px; line-height:1.5; }
.pm .next-track {
margin:16px auto 0; padding:8px 20px; border:none; background:#e10600; color:#fff;
font-size:12px; font-weight:700; font-family:'Inter',monospace; cursor:pointer;
transition:all 0.3s; letter-spacing:1px; border-radius:2px;
}
.pm .next-track:hover { background:#aa0000; }
/* ===== Example / Step-through ===== */
.eg-box {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #e10600; padding:20px; margin-bottom:8px; border-radius:2px;
position:relative;
}
.eg-box::before {
content:'EXAMPLE'; position:absolute; top:-7px; right:8px;
font-size:7px; font-weight:900; color:rgba(225,6,0,0.35); letter-spacing:2px;
}
.eg-title {
font-size:18px; font-weight:900; color:#fff; margin-bottom:14px;
letter-spacing:-0.3px; padding-bottom:10px;
border-bottom:1px solid rgba(255,255,255,0.03);
}
.eg-step {
padding:12px 16px; margin-bottom:8px; border-radius:2px;
border-left:3px solid transparent;
font-size:15px; line-height:1.8; color:rgba(255,255,255,0.7);
background:rgba(255,255,255,0.01);
animation:fIn 0.4s ease;
}
.eg-step.show { border-left-color:rgba(225,6,0,0.2); background:rgba(225,6,0,0.02); color:rgba(255,255,255,0.7); }
.eg-step .step-num {
display:inline-block; width:24px; height:24px; line-height:24px; text-align:center;
font-size:11px; font-weight:900; background:rgba(225,6,0,0.1); color:#e10600;
margin-right:10px; border-radius:2px; font-family:'Inter',monospace; float:left;
}
.eg-btn {
margin:14px 0 0; padding:10px 24px; border:1px solid rgba(225,6,0,0.15);
background:rgba(225,6,0,0.04); color:rgba(255,255,255,0.7); font-size:14px;
font-weight:700; font-family:'Inter',monospace; cursor:pointer;
transition:all 0.3s; border-radius:3px; letter-spacing:1px;
}
.eg-btn:hover { background:rgba(225,6,0,0.1); color:#fff; border-color:rgba(225,6,0,0.3); }
.eg-btn:disabled { opacity:0.3; cursor:default; }
/* ===== CONFETTI ===== */
.pyro { position:fixed; inset:0; pointer-events:none; z-index:99; overflow:hidden; }
.pyro .sp {
position:absolute; width:3px; height:3px; border-radius:0;
animation:spFall linear forwards;
}
@keyframes spFall { 0%{transform:translateY(-10px) scale(1);opacity:1} 100%{transform:translateY(100vh) scale(0);opacity:0} }
/* ===== RESPONSIVE ===== */
@media (max-width:600px) {
.app { padding:12px; }
.track-wrap { max-height:460px; }
.pm { padding:28px 20px 24px; }
.pm .pn1 { font-size:42px; }
}
/* === Track car move animation === */
.track-car.moving { animation:boost 1.2s ease; }
@keyframes boost {
0% { filter:drop-shadow(0 0 8px rgba(225,6,0,0.4)); }
40% { filter:drop-shadow(0 0 20px rgba(255,215,0,0.6)); }
100% { filter:drop-shadow(0 0 8px rgba(225,6,0,0.4)); }
}
/* 错题集 UI */
.hudBtn {
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.06);
color:rgba(255,255,255,0.5); font-size:11px; font-family:inherit; font-weight:700;
padding:8px 14px; cursor:pointer; letter-spacing:1px; border-radius:3px;
transition:all 0.2s; display:inline-flex; align-items:center; gap:4px;
}
.hudBtn:hover { background:rgba(255,255,255,0.06); color:rgba(255,255,255,0.8); }
.hudBtn #hintBadge {
background:#e10600; color:#fff; font-size:9px; font-weight:900;
min-width:18px; height:18px; line-height:18px; text-align:center; border-radius:9px;
padding:0 5px; display:inline-block;
}
.hudDanger:hover { border-color:rgba(225,6,0,0.2); color:#e10600; }
.wrItem {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.03);
border-left:3px solid #e10600; padding:14px 16px; margin-bottom:8px; border-radius:2px;
}
.wrH { font-size:10px; color:rgba(255,255,255,0.35); letter-spacing:1px; margin-bottom:6px; font-family:'Inter',monospace; }
.wrLb { color:rgba(255,255,255,0.45); }
.wrQ { font-size:15px; color:rgba(255,255,255,0.75); margin-bottom:10px; line-height:1.5; }
.wrAct { display:flex; gap:8px; }
.wrDo, .wrRm {
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.08);
color:rgba(255,255,255,0.5); font-size:10px; font-family:inherit; font-weight:700;
padding:6px 12px; cursor:pointer; letter-spacing:1px; border-radius:3px;
transition:all 0.2s;
}
.wrDo:hover { background:rgba(46,204,113,0.04); color:#2ecc71; border-color:rgba(46,204,113,0.1); }
.wrRm:hover { background:rgba(225,6,0,0.04); color:#e10600; border-color:rgba(225,6,0,0.1); }
.geoDiag { margin-bottom:10px; }
.geoDiag svg { display:block; max-height:160px; }
/* Answer map grid */
.mBtn2 {
padding:10px 20px; border:1px solid; font-size:13px; font-weight:700;
font-family:'Inter',monospace; cursor:pointer; border-radius:3px;
letter-spacing:1px; transition:all 0.2s; color:#fff;
}
.mBtn2:hover { filter:brightness(1.2); transform:translateY(-1px); }
.qGrid {
display:grid; grid-template-columns:repeat(auto-fill,minmax(64px,1fr));
gap:8px; padding:4px 0;
}
.qCell {
background:rgba(8,8,20,0.5); border:1px solid rgba(255,255,255,0.04);
border-left:3px solid rgba(255,255,255,0.06); border-radius:3px;
padding:10px 4px; text-align:center; cursor:pointer;
transition:all 0.2s; display:flex; flex-direction:column; align-items:center; gap:4px;
}
.qCell:hover { background:rgba(255,255,255,0.04); border-color:rgba(255,255,255,0.1); }
.qCell .qNum { font-size:14px; font-weight:700; color:rgba(255,255,255,0.4); font-family:'Inter',monospace; }
.qCell .qSt { font-size:12px; }
.qCell.correct { border-left-color:#2ecc71; background:rgba(46,204,113,0.04); }
.qCell.correct .qNum { color:#2ecc71; }
.qCell.wrong { border-left-color:#e10600; background:rgba(225,6,0,0.03); }
.qCell.wrong .qNum { color:#e10600; }
.qCell.unanswered .qNum { color:rgba(255,255,255,0.2); }
/* 分站赛 UI */
.stgBar { height:4px; background:rgba(255,255,255,0.04); border-radius:2px; margin:10px 0; overflow:hidden; }
.stgFill { height:100%; background:#e10600; border-radius:2px; transition:width 0.5s cubic-bezier(.34,1.56,.64,1); }
.stgTag { display:inline-block; padding:3px 10px; border:1px solid rgba(255,255,255,0.06); border-radius:2px; font-size:11px; font-weight:700; color:rgba(255,255,255,0.4); margin:2px 4px; font-family:'Inter',monospace; }
.stgTag:nth-child(1) { border-color:rgba(225,6,0,0.15); color:#e10600; }
.stgTag:nth-child(2) { border-color:rgba(255,215,0,0.15); color:#ffd700; }
.stgTag:nth-child(3) { border-color:rgba(46,204,113,0.15); color:#2ecc71; }
/* 口诀 */
.kj {
margin:12px 0 4px; padding:10px 14px; background:rgba(255,215,0,0.03);
border:1px solid rgba(255,215,0,0.06); border-left:3px solid #ffd700; border-radius:2px;
font-size:14px; line-height:1.9; color:rgba(255,255,255,0.8);
}
.kj::before { content:'\u{1F3B6} 口诀 '; font-weight:900; color:#ffd700; font-size:12px; letter-spacing:1px; display:block; margin-bottom:2px; }
/* 巧解 */
.qj {
margin:12px 0 4px; padding:10px 14px; background:rgba(46,204,113,0.03);
border:1px solid rgba(46,204,113,0.06); border-left:3px solid #2ecc71; border-radius:2px;
font-size:14px; line-height:1.9; color:rgba(255,255,255,0.8);
}
.qj::before { content:'\u{1F4A1} 巧解 '; font-weight:900; color:#2ecc71; font-size:12px; letter-spacing:1px; display:block; margin-bottom:2px; }
/* SVG 动画 */
@keyframes svgPulse { 0%,100%{opacity:0.3} 50%{opacity:1} }
@keyframes svgRotate { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
@keyframes svgDash { to{stroke-dashoffset:0} }
.animPulse { animation:svgPulse 2s ease infinite; }
.animRotate { transform-origin:center; animation:svgRotate 6s linear infinite; }
.animDash { stroke-dasharray:200; stroke-dashoffset:200; animation:svgDash 2s ease forwards; }
/* === Interactive Drag Models === */
.drag-point { cursor:grab; }
.drag-point:active { cursor:grabbing; }
.drag-point:hover { filter:drop-shadow(0 0 8px rgba(225,6,0,0.6)); }
.drag-point .dp-ring { transition:r 0.15s; }
.drag-point:hover .dp-ring { r:7; }
.m-panel { margin:8px 0; padding:10px 14px; background:rgba(0,0,0,0.15); border-radius:4px; display:flex; flex-wrap:wrap; gap:6px 14px; justify-content:center; }
.m-val { display:inline-flex; align-items:center; gap:4px; font-size:13px; font-weight:700; font-family:'Inter',monospace; }
.m-val .lbl { color:rgba(255,255,255,0.3); font-size:10px; letter-spacing:1px; }
.m-val .num { color:#ffd700; font-size:14px; }
.m-val .num.hl { color:#e10600; }
.m-hint { text-align:center; font-size:11px; color:rgba(255,255,255,0.25); letter-spacing:1px; margin:4px 0; font-family:'Inter',monospace; animation:pulse 2s ease infinite; }
.m-discover { text-align:center; font-size:14px; color:#2ecc71; font-weight:700; margin:4px 0; animation:slideUp 0.5s ease; }
.drag-line { cursor:pointer; }
</style>
</head>
<body>
<div class="app" id="app">
<!-- HUD -->
<div class="hud">
<div class="logo">
<span class="r">RED&nbsp;BULL</span>
<span class="w">&nbsp;/&nbsp;</span>
<span class="y">MV1</span>
</div>
<div class="hud-spacer"></div>
<div class="hud-stat"><div class="lbl">STAGE</div><div class="val y" id="hStage">1/4</div></div>
<div class="hud-stat"><div class="lbl">PTS</div><div class="val r" id="hPts">0</div></div>
<button class="mbtn" id="mBtn">&#x266B;</button>
</div>
<!-- HOME — TRACK MAP -->
<div class="pg act" id="pH">
<div class="track-wrap" id="trackWrap">
<span class="track-car" id="trackCar">
<img src="rbcar.png" class="ci" onerror="this.style.display='none';this.nextElementSibling.classList.remove('hide')" onload="this.nextElementSibling.classList.add('hide')" style="display:none">
<svg viewBox="0 0 60 28" class="cs" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bodyGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a0a3a"/>
<stop offset="100%" stop-color="#12124a"/>
</linearGradient>
<linearGradient id="wingGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e10600"/>
<stop offset="100%" stop-color="#cc0500"/>
</linearGradient>
</defs>
<rect x="1" y="5" width="6" height="18" rx="1" fill="url(#wingGrad)"/>
<rect x="0" y="4" width="3" height="3" rx="0.5" fill="#ffd700"/>
<rect x="0" y="21" width="3" height="3" rx="0.5" fill="#ffd700"/>
<rect x="4" y="4" width="4" height="20" rx="1" fill="#0a0a3a" stroke="#e10600" stroke-width="0.3"/>
<rect x="6" y="10" width="4" height="8" rx="0.5" fill="#1a1a3a"/>
<path d="M 12,7 L 24,6 Q 28,6 30,8 L 30,20 Q 28,22 24,22 L 12,21 Z" fill="url(#bodyGrad)"/>
<path d="M 22,6 Q 26,3 32,4 L 32,6 L 24,8 Z" fill="#e10600" opacity="0.3"/>
<path d="M 22,22 Q 26,25 32,24 L 32,22 L 24,20 Z" fill="#e10600" opacity="0.3"/>
<path d="M 24,4 L 34,4 Q 38,4 40,8 L 40,10 L 24,9 Z" fill="#14144a"/>
<path d="M 24,24 L 34,24 Q 38,24 40,20 L 40,18 L 24,19 Z" fill="#14144a"/>
<path d="M 30,8 Q 36,7 40,9 L 44,10 Q 48,10 50,11 L 52,13 L 52,15 L 50,17 Q 48,18 44,18 L 40,19 Q 36,21 30,20 Z" fill="url(#bodyGrad)"/>
<path d="M 50,11 L 56,12.5 L 57,14 L 56,15.5 L 50,17 Z" fill="#0e0e3e"/>
<path d="M 56,12.5 L 59,13.5 L 59,14.5 L 56,15.5 Z" fill="#e10600"/>
<path d="M 52,9 L 60,10 L 60,18 L 52,19 Z" fill="#0a0a3a" stroke="#e10600" stroke-width="0.3"/>
<rect x="58" y="8" width="2" height="4" rx="0.5" fill="#e10600"/>
<rect x="58" y="16" width="2" height="4" rx="0.5" fill="#e10600"/>
<ellipse cx="42" cy="14" rx="4" ry="4.5" fill="none" stroke="#ffd700" stroke-width="0.8" opacity="0.8"/>
<rect x="16" y="2" width="3" height="4" rx="1" fill="#1a1a1a"/>
<rect x="16" y="22" width="3" height="4" rx="1" fill="#1a1a1a"/>
<rect x="38" y="2.5" width="3" height="3.5" rx="1" fill="#1a1a1a"/>
<rect x="38" y="22" width="3" height="3.5" rx="1" fill="#1a1a1a"/>
<rect x="17" y="3" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="17" y="23" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="39" y="3" width="1" height="2" rx="0.5" fill="#e10600"/>
<rect x="39" y="23" width="1" height="2" rx="0.5" fill="#e10600"/>
<text x="35" y="16" font-family="'Inter',Arial,sans-serif" font-weight="900" font-size="7" fill="#ffd700" text-anchor="middle">1</text>
<text x="20" y="20" font-family="'Inter',Arial,sans-serif" font-weight="900" font-size="2.5" fill="#e10600" text-anchor="middle" opacity="0.6">RED BULL</text>
<rect x="8" y="11" width="3" height="6" rx="0.5" fill="#222"/>
<circle cx="10" cy="14" r="1.5" fill="#333"/>
</svg>
</span>
<svg viewBox="0 0 360 540" preserveAspectRatio="xMidYMid meet">
<!-- S-curve track — road surface -->
<path class="track-road" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- track edges -->
<path class="track-bg" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- remaining circuit (dashed center line) -->
<path class="track-fill" id="trackFill" d="M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520"/>
<!-- completed portion (gold) -->
<path class="track-done" id="trackDone" d="M 180,20" stroke-dasharray="800" stroke-dashoffset="800"/>
<!-- Finish line -->
<line x1="160" y1="515" x2="200" y2="515" stroke="#ffd700" stroke-width="2" opacity="0.15"/>
<line x1="160" y1="520" x2="200" y2="520" stroke="#ffd700" stroke-width="2" opacity="0.1"/>
<line x1="160" y1="525" x2="200" y2="525" stroke="#ffd700" stroke-width="2" opacity="0.08"/>
<!-- CP 1 — 有理数 -->
<g class="cp" data-idx="0">
<circle cx="280" cy="68" r="24" fill="transparent" pointer-events="all"/>
<circle cx="280" cy="68" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="280" y="72">SS 1</text>
<text class="cp-name" x="280" y="40">有理数</text>
<text class="cp-stars" id="cps0" x="280" y="90"></text>
<text class="lock-icon" x="280" y="68" style="display:none">&#x1F512;</text>
</g>
<!-- CP 2 — 整式的加减 -->
<g class="cp" data-idx="1">
<circle cx="80" cy="200" r="24" fill="transparent" pointer-events="all"/>
<circle cx="80" cy="200" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="80" y="204">SS 2</text>
<text class="cp-name" x="80" y="172">整式的加减</text>
<text class="cp-stars" id="cps1" x="80" y="222"></text>
<text class="lock-icon" x="80" y="200" style="display:none">&#x1F512;</text>
</g>
<!-- CP 3 — 一元一次方程 -->
<g class="cp" data-idx="2">
<circle cx="280" cy="340" r="24" fill="transparent" pointer-events="all"/>
<circle cx="280" cy="340" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="280" y="344">SS 3</text>
<text class="cp-name" x="280" y="312">一元一次方程</text>
<text class="cp-stars" id="cps2" x="280" y="362"></text>
<text class="lock-icon" x="280" y="340" style="display:none">&#x1F512;</text>
</g>
<!-- CP 4 — 几何图形初步 -->
<g class="cp" data-idx="3">
<circle cx="120" cy="468" r="24" fill="transparent" pointer-events="all"/>
<circle cx="120" cy="468" r="14" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.3)" stroke-width="2"/>
<text class="cp-label" x="120" y="472">SS 4</text>
<text class="cp-name" x="120" y="440">几何图形初步</text>
<text class="cp-stars" id="cps3" x="120" y="490"></text>
<text class="lock-icon" x="120" y="468" style="display:none">&#x1F512;</text>
</g>
</svg>
</div>
<div style="text-align:center;margin-top:4px;">
<div style="display:inline-block;background:rgba(225,6,0,0.03);border:1px solid rgba(225,6,0,0.05);padding:5px 16px;border-radius:2px;">
<span style="font-size:9px;color:rgba(255,255,255,0.35);letter-spacing:2px;font-weight:700;">
&#x26A1; 点击检查点发车 &nbsp;|&nbsp; 四站全胜 = 世界冠军
</span>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:center;margin-top:6px;flex-wrap:wrap;">
<button class="hudBtn" id="wrongBtn"><span id="hintBadge">0</span>\u{1F4D2} 错题集</button>
<button class="hudBtn hudDanger" id="clrP">\u{1F5D1} 清除进度</button>
</div>
<div style="text-align:center;margin-top:6px;margin-bottom:4px;">
<button class="hudBtn" id="dbBtn" style="padding:8px 16px;font-size:10px;">\u{1F4CA} 学习总览</button>
</div>
<div id="dbPanel" style="display:none;margin-top:8px;"></div>
</div>
<!-- CHAPTER -->
<div class="pg" id="pC">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;">
<button class="bk" id="bkMap" style="margin-bottom:0">&larr; 赛道</button>
<button class="bk" id="bkMapBtn" style="margin-bottom:0;background:rgba(255,215,0,0.06);border-color:rgba(255,215,0,0.1);color:#ffd700;">&#x1F4CA; 答题地图</button>
</div>
<div class="ch">
<div class="gl" id="cGl">SS 1</div>
<h2 id="cTi"></h2>
<div class="sm" id="cDe"></div>
</div>
<div id="cCo"></div>
</div>
<!-- ANSWER MAP -->
<div class="pg" id="pM">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;">
<button class="bk" id="bkMapM" style="margin-bottom:0">&larr; 赛道</button>
<button class="bk" id="bkChapM" style="margin-bottom:0">&larr; 返回</button>
</div>
<div class="ch">
<div class="gl" id="mGl">答题地图</div>
<h2 id="mTi"></h2>
<div class="sm" id="mDe">点击题目跳转</div>
</div>
<div id="mGrid"></div>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:16px;">
<button class="mBtn2" id="mUn" style="background:#ffd700;color:#0a0a1a;border-color:#ffd700;">&#x25B6; 从未答开始</button>
<button class="mBtn2" id="mErr" style="background:#e10600;border-color:#e10600;">&#x2716; 从错题开始</button>
<button class="mBtn2" id="mAll" style="background:rgba(255,255,255,0.05);border-color:rgba(255,255,255,0.1);">&#x1F504; 全部重做</button>
</div>
</div>
<!-- WRONG REVIEW -->
<div class="pg" id="pW">
<button class="bk" id="bkMapW">&larr; 赛道</button>
<div class="ch"><div class="gl" id="wGl">\u{1F4D2} 错题集</div><h2>错题再练</h2><div class="sm" id="wDe">错了不怕,练到对为止</div></div>
<div id="wList"></div>
</div>
</div>
<!-- PODIUM -->
<div class="po" id="po">
<div class="pm">
<img src="mv1.png" class="mv1-photo" id="mv1Img" onerror="this.classList.add('hide');document.getElementById('mv1Av').classList.remove('hide')" onload="document.getElementById('mv1Av').classList.add('hide')">
<div class="mv1-av hide" id="mv1Av">MV1</div>
<div class="pn1" id="pmN">P1</div>
<span class="pcup" id="pmC">&#x1F3C6;</span>
<h2 id="pmTi">冠军</h2>
<div class="ps" id="pmSt"></div>
<div class="pxp" id="pmXp">+0 PTS</div>
<p class="pms" id="pmMs">下个赛段继续冲</p>
<button class="next-track" id="pmNt">下一站 &rarr;</button>
</div>
</div>
<div class="pyro" id="pyro"></div>
<script>
// ===== 几何可视化引擎(绘图工具)=====
const Geo = {
svg(w,h,inner){return `<svg viewBox="0 0 ${w} ${h}" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">${inner}</svg>`;},
label(x,y,t,o={}){const{fs=10,fill='rgba(255,255,255,0.7)',anchor='middle',fw='400'}=o;return `<text x="${x}" y="${y}" font-size="${fs}" fill="${fill}" text-anchor="${anchor}" font-weight="${fw}" font-family="'Inter',monospace">${t}</text>`;},
dot(x,y,o={}){const{r=3,fill='#ffd700'}=o;return `<circle cx="${x}" cy="${y}" r="${r}" fill="${fill}"/>`;},
line(x1,y1,x2,y2,o={}){const{sw=2,clr='rgba(255,255,255,0.12)',dash=''}=o;return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${clr}" stroke-width="${sw}" stroke-dasharray="${dash}"/>`;},
arrow(x1,y1,x2,y2,o={}){const{sw=2,clr='rgba(255,255,255,0.3)',sz=6}=o;const dx=x2-x1,dy=y2-y1,len=Math.hypot(dx,dy);if(!len)return '';const ux=dx/len,uy=dy/len;return this.line(x1,y1,x2,y2,o)+`<polygon points="${x2},${y2} ${x2-sz*ux+sz*uy/2},${y2-sz*uy-sz*ux/2} ${x2-sz*ux-sz*uy/2},${y2-sz*uy+sz*ux/2}" fill="${clr}"/>`;},
seg(x,y,w,o={}){const{la='A',lb='B',col='#ffd700'}=o;let r=this.dot(x,y,{fill:col})+this.dot(x+w,y,{fill:col})+this.line(x,y,x+w,y,{clr:'rgba(255,255,255,0.4)',sw:2});return r+this.label(x,y+18,la,{fs:9,fill:col})+this.label(x+w,y+18,lb,{fs:9,fill:col});},
midSeg(x,y,w,o={}){const{la='A',lb='B',ml='M',ac='',cb='',total=''}=o;const mx=x+w/2;return this.seg(x,y,w,{la,lb})+this.dot(mx,y,{fill:'#e10600'})+this.label(mx,y-8,ml,{fs:8,fill:'#e10600'})+(ac?this.label(x+w/4,y-23,ac,{fs:8,fill:'rgba(255,255,255,0.2)'}):'')+(cb?this.label(x+w*3/4,y-23,cb,{fs:8,fill:'rgba(255,255,255,0.2)'}):'')+(total?this.label(x+w/2,y+30,total,{fs:9,fill:'#ffd700'}):'');},
aArc(cx,cy,r,a,o={}){const{clr='#ffd700',sw=2,deg='',tag='',fs=11}=o;const ar=a*Math.PI/180;const pts=[];for(let i=0;i<=40;i++){const t=i/40*ar;pts.push(`${cx+r*Math.cos(Math.PI/2-t)},${cy-r*Math.sin(Math.PI/2-t)}`)}let s=`<polyline points="${pts.join(' ')}" fill="none" stroke="${clr}" stroke-width="${sw}"/>`;if(deg)s+=this.label(cx+r*Math.cos(Math.PI/2-ar/2)*1.3,cy-r*Math.sin(Math.PI/2-ar/2)*1.3,deg+'°',{fs,fill:clr,fw:'900'});if(tag)s+=this.label(cx+r*Math.cos(Math.PI/2-ar/2)*0.7,cy-r*Math.sin(Math.PI/2-ar/2)*0.7,tag,{fs:9,fill:clr});return s;},
rAngle(cx,cy,sz,up){return `<polyline points="${cx+sz},${cy} ${cx+sz},${cy-sz*(up||1)} ${cx},${cy-sz*(up||1)}" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>`;},
clock(cx,cy,r,h,m,o={}){const{hl=''}=o;const hAng=(h%12)*30+m*0.5,mAng=m*6;let s=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>`;for(let i=0;i<12;i++){const a=i*30*Math.PI/180;const x1=cx+(r-6)*Math.sin(a),y1=cy-(r-6)*Math.cos(a);const x2=cx+r*Math.sin(a),y2=cy-r*Math.cos(a);s+=this.line(x1,y1,x2,y2,{clr:`rgba(255,255,255,${i%3===0?0.12:0.04})`,sw:i%3===0?1.5:1});if(i%3===0){const lx=cx+(r+14)*Math.sin(a),ly=cy-(r+14)*Math.cos(a);s+=this.label(lx,ly+3,i||12,{fs:8,fill:'rgba(255,255,255,0.2)'})}}const hh=r*0.45,hm=r*0.6;const ha=(hAng-90)*Math.PI/180,ma=(mAng-90)*Math.PI/180;s+=this.line(cx,cy,cx+hh*Math.cos(ha),cy+hh*Math.sin(ha),{clr:'#ffd700',sw:3});s+=this.line(cx,cy,cx+hm*Math.cos(ma),cy+hm*Math.sin(ma),{clr:'rgba(255,255,255,0.2)',sw:1.5});s+=this.dot(cx,cy,{r:4,fill:'#e10600'});if(hl)s+=this.label(cx,cy-r-16,hl,{fs:9,fill:'#ffd700',fw:'900'});return s;},
compass(cx,cy,r,deg,o={}){const{label=''}=o;let s=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>`;const dirs=[{d:'北',a:0},{d:'东',a:90},{d:'南',a:180},{d:'西',a:270}];dirs.forEach(({d,a})=>{const t=a*Math.PI/180;const x=cx+(r+12)*Math.sin(t),y=cy-(r+12)*Math.cos(t);s+=this.label(x,y+3,d,{fs:9,fill:a===0?'#e10600':'rgba(255,255,255,0.2)',fw:a===0?'900':'400'})});const a=deg*Math.PI/180;const ex=cx+r*Math.sin(a),ey=cy-r*Math.cos(a);s+=this.arrow(cx,cy,ex,ey,{clr:'#ffd700',sw:2.5});s+=this.dot(cx,cy,{r:3,fill:'#ffd700'});if(label)s+=this.label(ex+10,ey-5,label,{fs:9,fill:'#ffd700'});return s;},
numLine(cx,cy,w,vals,o={}){const{labels={}}=o;const mn=Math.min(...vals),mx=Math.max(...vals),rg=mx-mn||1;let s=this.arrow(cx,cy,cx+w,cy,{clr:'rgba(255,255,255,0.2)',sw:1.5});vals.forEach(v=>{const x=cx+(v-mn)/rg*w;if(v===0)s+=this.dot(x,cy,{r:3,fill:'#ffd700'})+this.label(x,cy+12,'0',{fs:9,fill:'#ffd700',fw:'900'});else s+=this.dot(x,cy,{r:2,fill:'rgba(255,255,255,0.15)'})+this.label(x,cy+12,v,{fs:8,fill:'rgba(255,255,255,0.2)'});if(labels[v])s+=this.label(x,cy-8,labels[v],{fs:7,fill:'#e10600'})});return s;}
};
// ===== 交互式动态几何模型引擎 =====
const GeoModel = {
// ---- 拖拽状态 ----
drag:{active:false,type:'',el:null,startX:0,startY:0,bx:0,by:0,minX:0,maxX:0,minY:0,maxY:0,ox:0,oy:0,cb:null},
startDrag(e,type,el,opts){
const r=el.getBoundingClientRect();const svg=el.closest('svg');const sR=svg.getBoundingClientRect();
const sx=sR.width/svg.viewBox.animVal.width, sy=sR.height/svg.viewBox.animVal.height;
this.drag={active:true,type,el,startX:(e.clientX-sR.left)/sx,startY:(e.clientY-sR.top)/sy,bx:0,by:0,
minX:opts.minX||0,maxX:opts.maxX||360,minY:opts.minY||0,maxY:opts.maxY||200,
ox:parseFloat(el.getAttribute('cx')),oy:parseFloat(el.getAttribute('cy')),
sx,sy,sr:sR,cb:opts.cb||null};
el.classList.add('dragging');
},
moveDrag(e){
const d=this.drag;if(!d.active)return;
const r=d.sr;const x=(e.clientX-r.left)/d.sx,y=(e.clientY-r.top)/d.sy;
const nx=Math.max(d.minX,Math.min(d.maxX,x)), ny=Math.max(d.minY,Math.min(d.maxY,y));
d.el.setAttribute('cx',nx);d.el.setAttribute('cy',ny);
d.bx=nx-d.ox;d.by=ny-d.oy;
if(d.cb)d.cb(nx,ny);
},
endDrag(){const d=this.drag;if(d.active){d.el.classList.remove('dragging');this.drag.active=false;}},
// ---- 双中点拖拽模型 ----
renderMidpoint(){
const w=360,h=180,ax=40,ay=70,bx=320,by=70,cxInit=180,ab=bx-ax;
const scaler=function(v){return Math.round(v/ab*100)};const fmt=function(v){return scaler(v)};
const initCx=cxInit;
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽点C——观察MN的变化</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动橙色点C</span>
</div>
<svg viewBox="0 0 ${w} ${h}" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<!-- AB线段 -->
<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${by}" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<!-- A点 -->
<circle cx="${ax}" cy="${ay}" r="5" fill="#ffd700"/><text x="${ax}" y="${ay+18}" font-size="11" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">A</text>
<!-- B点 -->
<circle cx="${bx}" cy="${ay}" r="5" fill="#ffd700"/><text x="${bx}" y="${ay+18}" font-size="11" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">B</text>
<!-- C点(可拖拽) -->
<circle class="drag-point" data-model="mid" data-point="C" cx="${initCx}" cy="${ay}" r="7" fill="#e10600" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<text x="${initCx}" y="${ay-14}" font-size="10" fill="#e10600" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">C</text>
<!-- 动态M点 -->
<circle id="mPt" cx="${ax+(initCx-ax)/2}" cy="${ay}" r="5" fill="#2ecc71"/>
<text id="mPtLbl" x="${ax+(initCx-ax)/2}" y="${ay-14}" font-size="10" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">M</text>
<!-- 动态N点 -->
<circle id="nPt" cx="${initCx+(bx-initCx)/2}" cy="${ay}" r="5" fill="#2ecc71"/>
<text id="nPtLbl" x="${initCx+(bx-initCx)/2}" y="${ay-14}" font-size="10" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">N</text>
<!-- 测量标注(实时更新) -->
<text id="mAC" x="${ax+(initCx-ax)/4}" y="${ay+35}" font-size="9" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">AC=${fmt(initCx-ax)}</text>
<text id="mCB" x="${initCx+(bx-initCx)/4}" y="${ay+35}" font-size="9" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">CB=${fmt(bx-initCx)}</text>
<!-- 发现区 -->
<text id="mDisc" x="${w/2}" y="${ay+55}" font-size="0" fill="#2ecc71" text-anchor="middle" font-weight="700" font-family="'Inter',monospace"></text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">AB</span><span class="num" id="vAB">${fmt(ab)}</span></span>
<span class="m-val"><span class="lbl">AC</span><span class="num" id="vAC">${fmt(initCx-ax)}</span></span>
<span class="m-val"><span class="lbl">CB</span><span class="num" id="vCB">${fmt(bx-initCx)}</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">MN</span><span class="num hl" id="vMN">${fmt(ab/2)}</span></span>
</div>
</div>`;
},
initMidDrag(cx,cy){
const ax=40,bx=320,ay=70;
const nx=Math.round(Math.max(ax+15,Math.min(bx-15,cx)));
const mAC=nx-ax,mCB=bx-nx,mAB=bx-ax,mMN=mAB/2;
const scaler=function(v){return Math.round(v/mAB*100)};
const mCx=(ax+nx)/2,mNx=(nx+bx)/2;
$('mPt')?.setAttribute('cx',mCx);$('mPtLbl')?.setAttribute('x',mCx);
$('nPt')?.setAttribute('cx',mNx);$('nPtLbl')?.setAttribute('x',mNx);
$('mAC')?.setAttribute('x',ax+(nx-ax)/4);$('mAC').textContent='AC='+scaler(mAC);
$('mCB')?.setAttribute('x',nx+(bx-nx)/4);$('mCB').textContent='CB='+scaler(mCB);
$('vAC').textContent=scaler(mAC);$('vCB').textContent=scaler(mCB);
$('vAB').textContent=scaler(mAB);$('vMN').textContent=scaler(mMN);
const disc=$('mDisc');
if(disc){disc.setAttribute('font-size','13');disc.textContent='\u{2728} 发现:无论C在哪里,MN总是AB的一半!';}
},
// ---- 角平分线拖拽模型 ----
renderAngleBisector(){
const cx=180,cy=80,r=60,initDeg=60;
const rad=initDeg*Math.PI/180;
const bx=cx+r*Math.cos(Math.PI/2-rad),by=cy-r*Math.sin(Math.PI/2-rad);
const bisDeg=initDeg/2;
const bisRad=bisDeg*Math.PI/180;
const bisx=cx+r*Math.cos(Math.PI/2-bisRad),bisy=cy-r*Math.sin(Math.PI/2-bisRad);
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽OB旋转——观察角平分线</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动B点旋转</span>
</div>
<svg viewBox="0 0 360 170" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<!-- OA射线(固定水平) -->
<line x1="${cx}" y1="${cy}" x2="${cx+r+20}" y2="${cy}" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
<polygon points="${cx+r+18},${cy-5} ${cx+r+24},${cy} ${cx+r+18},${cy+5}" fill="rgba(255,255,255,0.15)"/>
<text x="${cx+r+26}" y="${cy+4}" font-size="10" fill="rgba(255,255,255,0.3)" font-family="'Inter',monospace">A</text>
<!-- OB射线(可旋转) -->
<line id="obRay" x1="${cx}" y1="${cy}" x2="${bx}" y2="${by}" stroke="#e10600" stroke-width="2.5"/>
<polygon id="obArr" points="${bx},${by} ${bx-6*Math.cos(Math.PI/2-rad)-3*Math.sin(Math.PI/2-rad)},${by+6*Math.sin(Math.PI/2-rad)-3*Math.cos(Math.PI/2-rad)} ${bx-6*Math.cos(Math.PI/2-rad)+3*Math.sin(Math.PI/2-rad)},${by+6*Math.sin(Math.PI/2-rad)+3*Math.cos(Math.PI/2-rad)}" fill="#e10600"/>
<!-- B点(可拖拽) -->
<circle class="drag-point" data-model="angle" data-point="B" cx="${bx}" cy="${by}" r="7" fill="#e10600" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<text x="${bx+10}" y="${by-5}" font-size="10" fill="#e10600" text-anchor="start" font-weight="900" font-family="'Inter',monospace">B</text>
<!-- O点 -->
<circle cx="${cx}" cy="${cy}" r="5" fill="#ffd700"/>
<text x="${cx-8}" y="${cy+16}" font-size="10" fill="#ffd700" text-anchor="middle" font-weight="700" font-family="'Inter',monospace">O</text>
<!-- 角平分线OC -->
<line id="bisRay" x1="${cx}" y1="${cy}" x2="${bisx}" y2="${bisy}" stroke="#2ecc71" stroke-width="2.5" stroke-dasharray="5,3"/>
<text id="bisLbl" x="${bisx+10}" y="${bisy-5}" font-size="10" fill="#2ecc71" font-family="'Inter',monospace">C(平分线)</text>
<!-- 角度弧 -->
<path id="aArc" d="${this.arcPath(cx,cy,40,initDeg)}" stroke="#ffd700" stroke-width="2" fill="none" opacity="0.6"/>
<text id="degLbl" x="${cx+45}" y="${cy-10}" font-size="12" fill="#ffd700" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">${initDeg}°</text>
<!-- 平分角弧 -->
<path id="bisArc" d="${this.arcPath(cx,cy,25,bisDeg)}" stroke="#2ecc71" stroke-width="1.5" fill="none" opacity="0.4"/>
<text id="bisDegLbl" x="${cx+32}" y="${cy-8}" font-size="10" fill="#2ecc71" text-anchor="middle" font-family="'Inter',monospace">${bisDeg}°</text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">∠AOB</span><span class="num" id="vAOB">${initDeg}°</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">∠AOC</span><span class="num hl" id="vAOC">${bisDeg}°</span></span>
<span class="m-val"><span class="lbl" style="color:#2ecc71;">∠COB</span><span class="num hl" id="vCOB">${bisDeg}°</span></span>
</div>
</div>`;
},
arcPath(cx,cy,r,deg){
return this.arcPts(cx,cy,r,0,deg);
},
initAngleDrag(cx,cy){
const ocx=180,ocy=80,r=60;
const dx=cx-ocx,dy=cy-ocy;
const rawDeg=Math.atan2(-dy,dx)*180/Math.PI+90;
let deg=Math.max(10,Math.min(170,rawDeg));
const rad=deg*Math.PI/180;
const bx=ocx+r*Math.cos(Math.PI/2-rad),by=ocy-r*Math.sin(Math.PI/2-rad);
const bisDeg=deg/2,bisRad=bisDeg*Math.PI/180;
const bisx=ocx+r*Math.cos(Math.PI/2-bisRad),bisy=ocy-r*Math.sin(Math.PI/2-bisRad);
const ray=$('obRay');if(ray){ray.setAttribute('x2',bx);ray.setAttribute('y2',by);}
const arr=$('obArr');if(arr){
const ax=bx-6*Math.cos(Math.PI/2-rad)-3*Math.sin(Math.PI/2-rad),ay=by+6*Math.sin(Math.PI/2-rad)-3*Math.cos(Math.PI/2-rad);
const bx2=bx-6*Math.cos(Math.PI/2-rad)+3*Math.sin(Math.PI/2-rad),by2=by+6*Math.sin(Math.PI/2-rad)+3*Math.cos(Math.PI/2-rad);
arr.setAttribute('points',`${bx},${by} ${ax},${ay} ${bx2},${by2}`);
}
const bisEl=$('bisRay');if(bisEl){bisEl.setAttribute('x2',bisx);bisEl.setAttribute('y2',bisy);}
const bisLblEl=$('bisLbl');if(bisLblEl){bisLblEl.setAttribute('x',bisx+10);bisLblEl.setAttribute('y',bisy-5);}
const arcEl=$('aArc');if(arcEl)arcEl.setAttribute('d',this.arcPts(ocx,ocy,40,0,deg));
const bisArcEl=$('bisArc');if(bisArcEl)bisArcEl.setAttribute('d',this.arcPts(ocx,ocy,25,0,bisDeg));
const degLbl=$('degLbl');if(degLbl)degLbl.textContent=deg+'°';
const bisDegLbl=$('bisDegLbl');if(bisDegLbl)bisDegLbl.textContent=Math.round(bisDeg)+'°';
$('vAOB').textContent=deg+'°';$('vAOC').textContent=Math.round(bisDeg)+'°';$('vCOB').textContent=Math.round(bisDeg)+'°';
},
// ---- 钟面角交互模型 ----
renderClock(){
const cx=180,cy=80,r=60,h=8,m=30;
return `<div style="margin:6px 0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:13px;font-weight:700;color:#ffd700;">\u{1F3C6} 拖拽指针——观察钟面角</span>
<span class="m-hint" style="animation:pulse 2s ease infinite;font-size:10px;">\u{1F446} 拖动分针或时针</span>
</div>
<svg viewBox="0 0 360 170" style="width:100%;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
${[0,30,60,90,120,150].map(a=>{const t=a*Math.PI/180;const x1=cx+(r-8)*Math.sin(t),y1=cy-(r-8)*Math.cos(t);const x2=cx+r*Math.sin(t),y2=cy-r*Math.cos(t);return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="rgba(255,255,255,${a%90===0?0.15:0.05})" stroke-width="${a%90===0?2:1}"/>`}).join('')}
${[3,6,9,12].map(h=>{const a=h*30*Math.PI/180;const lx=cx+(r+14)*Math.sin(a),ly=cy-(r+14)*Math.cos(a);return `<text x="${lx}" y="${ly+4}" font-size="11" fill="rgba(255,255,255,0.2)" text-anchor="middle" font-family="'Inter',monospace">${h}</text>`}).join('')}
${[1,2,4,5,7,8,10,11].map(h=>{const a=h*30*Math.PI/180;const lx=cx+(r+10)*Math.sin(a),ly=cy-(r+10)*Math.cos(a);return `<text x="${lx}" y="${ly+3}" font-size="7" fill="rgba(255,255,255,0.08)" text-anchor="middle" font-family="'Inter',monospace">${h}</text>`}).join('')}
<!-- 时针(可拖拽) -->
<line id="cHh" data-h="8" x1="${cx}" y1="${cy}" x2="${cx+0.45*r*Math.sin((h*30+m*0.5-90)*Math.PI/180)}" y2="${cy-0.45*r*Math.cos((h*30+m*0.5-90)*Math.PI/180)}" stroke="#ffd700" stroke-width="3.5"/>
<circle class="drag-point" data-model="clock" data-hand="h" cx="${cx+0.45*r*Math.sin((h*30+m*0.5-90)*Math.PI/180)}" cy="${cy-0.45*r*Math.cos((h*30+m*0.5-90)*Math.PI/180)}" r="6" fill="#ffd700" stroke="#fff" stroke-width="1.5" cursor="grab"/>
<!-- 分针(可拖拽) -->
<line id="cMh" data-m="30" x1="${cx}" y1="${cy}" x2="${cx+0.6*r*Math.sin((m*6-90)*Math.PI/180)}" y2="${cy-0.6*r*Math.cos((m*6-90)*Math.PI/180)}" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
<circle class="drag-point" data-model="clock" data-hand="m" cx="${cx+0.6*r*Math.sin((m*6-90)*Math.PI/180)}" cy="${cy-0.6*r*Math.cos((m*6-90)*Math.PI/180)}" r="5" fill="rgba(255,255,255,0.3)" stroke="#fff" stroke-width="1" cursor="grab"/>
<!-- 中心点 -->
<circle cx="${cx}" cy="${cy}" r="4" fill="#e10600"/>
<!-- 角度弧 -->
<path id="cArc" d="" stroke="#ffd700" stroke-width="2" fill="none" opacity="0.5"/>
<text id="cAng" x="${cx}" y="${cy-r-20}" font-size="14" fill="#ffd700" text-anchor="middle" font-weight="900" font-family="'Inter',monospace">75°</text>
</svg>
<div class="m-panel">
<span class="m-val"><span class="lbl">时刻</span><span class="num" id="cTime">${h}:${String(m).padStart(2,'0')}</span></span>
<span class="m-val"><span class="lbl">时针</span><span class="num" id="cHDeg">${h*30+m*0.5}°</span></span>
<span class="m-val"><span class="lbl">分针</span><span class="num" id="cMDeg">${m*6}°</span></span>
<span class="m-val"><span class="lbl" style="color:#ffd700;">夹角</span><span class="num hl" id="cDiff">75°</span></span>
</div>
<div style="text-align:center;margin-top:4px;"><span style="font-size:11px;color:rgba(255,255,255,0.15);font-family:'Inter',monospace;">公式:|30h - 5.5m|</span></div>
</div>`;
},
initClockDrag(cx,cy,hand){
const ocx=180,ocy=80,r=60;
const dx=cx-ocx,dy=cy-ocy;
const rawDeg=Math.atan2(-dy,dx)*180/Math.PI+90;
let deg=rawDeg<0?rawDeg+360:rawDeg;
let h=parseInt($('cHh').getAttribute('data-h')||'8'), m=parseInt($('cMh').getAttribute('data-m')||'30');
if(hand==='h'){h=Math.round(((deg%360)/30)%12);if(h===0)h=12;m=Math.round((deg%30)*2);}
else{m=Math.round(deg/6)%60;let mAng=m*6;let hAng=h%12*30+m*0.5;let diff=Math.abs(hAng-mAng);if(diff>180)diff=360-diff;$('cArc').setAttribute('d',this.clockArc(ocx,ocy,40,Math.min(mAng,hAng),Math.max(mAng,hAng)));}
// Compute positions
const hAng=h%12*30+m*0.5,mAng=m*6;
const diff=Math.abs(hAng-mAng);const finalDiff=diff>180?360-diff:diff;
const hx=ocx+0.45*r*Math.sin((hAng-90)*Math.PI/180),hy=ocy-0.45*r*Math.cos((hAng-90)*Math.PI/180);
const mx=ocx+0.6*r*Math.sin((mAng-90)*Math.PI/180),my=ocy-0.6*r*Math.cos((mAng-90)*Math.PI/180);
const hPt=$('cHh');if(hPt){hPt.setAttribute('x2',hx);hPt.setAttribute('y2',hy);hPt.setAttribute('data-h',h);}
const mPt=$('cMh');if(mPt){mPt.setAttribute('x2',mx);mPt.setAttribute('y2',my);mPt.setAttribute('data-m',m);}
const hDrag=document.querySelector('[data-model="clock"][data-hand="h"]');if(hDrag){hDrag.setAttribute('cx',hx);hDrag.setAttribute('cy',hy);}
const mDrag=document.querySelector('[data-model="clock"][data-hand="m"]');if(mDrag){mDrag.setAttribute('cx',mx);mDrag.setAttribute('cy',my);}
$('cTime').textContent=h+':'+String(Math.round(m)).padStart(2,'0');
$('cHDeg').textContent=Math.round(hAng)+'°';$('cMDeg').textContent=Math.round(mAng)+'°';$('cDiff').textContent=Math.round(finalDiff)+'°';
$('cAng').textContent=Math.round(finalDiff)+'°';
const start=Math.min(hAng,mAng),end=Math.max(hAng,mAng);
const arc=this.clockArc(ocx,ocy,35,start,end);
const arcEl=$('cArc');if(arcEl)arcEl.setAttribute('d',arc);
},
clockArc(cx,cy,r,s,e){
const a1=Math.min(s,e),a2=Math.max(s,e);
if(a2-a1>180){return this.arcPts(cx,cy,r,a2,360)+' '+this.arcPts(cx,cy,r,0,a1);}
return this.arcPts(cx,cy,r,a1,a2);
},
arcPts(cx,cy,r,s,e){
const pts=[];for(let i=0;i<=24;i++){const t=(s+(e-s)*i/24)*Math.PI/180;pts.push(`${cx+r*Math.sin(t)},${cy-r*Math.cos(t)}`)}return pts.join(' ');
}
};
const SS = [
// ═══════════════════════════════════════
// CHAPTER 1: 有理数
// ═══════════════════════════════════════
{ id:1, title:'有理数', desc:'正负·数轴·相反·绝对值·运算·乘方·科学记数法', emoji:'🏁',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>📋 有理数 = "有理有据"的数</h3><p>有理数就是所有能写成 p/q(q≠0)的数——整数和分数都是有理数。</p><p>本章路线:正负数→数轴→相反数/绝对值→四则运算→倒数→乘方→科学记数法</p><svg viewBox="0 0 360 50" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="25" x2="342" y2="25" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="30" cy="25" r="5" fill="#e10600"/><text x="30" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">正负数</text><circle cx="90" cy="25" r="5" fill="#e10600"/><text x="90" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">数轴</text><circle cx="150" cy="25" r="5" fill="#e10600"/><text x="150" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">相反/绝对</text><circle cx="210" cy="25" r="5" fill="#e10600"/><text x="210" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">四则运算</text><circle cx="270" cy="25" r="5" fill="#e10600"/><text x="270" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">倒数/乘方</text><circle cx="330" cy="25" r="5" fill="#e10600"/><text x="330" y="42" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">科学记数法</text><line x1="35" y1="25" x2="85" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="95" y1="25" x2="145" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="155" y1="25" x2="205" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="215" y1="25" x2="265" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/><line x1="275" y1="25" x2="325" y2="25" stroke="rgba(255,255,255,0.04)" stroke-width="1"/></svg>` },
// ── KP1: 正数和负数 ──
{ type:'kp', icon:'🌡️', title:'正数和负数',
content:`<p>🔥 <strong>正数</strong>:大于0的数,如+3、5"+"可省略)</p>
<p>❄️ <strong>负数</strong>:小于0的数,前面带"-",如-3、-2.5</p>
<p>0既不是正数也不是负数——它是正负的<strong>分界线</strong></p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<rect x="160" y="5" width="16" height="35" rx="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<circle cx="168" cy="42" r="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<rect x="163" y="12" width="10" height="26" rx="4" fill="rgba(225,6,0,0.2)"/>
<circle cx="168" cy="42" r="6" fill="rgba(225,6,0,0.2)"/>
<text x="170" y="60" font-size="8" fill="#e10600" text-anchor="middle" font-weight="700">+30°C零上</text>
<rect x="10" y="5" width="16" height="35" rx="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<circle cx="18" cy="42" r="8" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<rect x="13" y="30" width="10" height="8" rx="4" fill="rgba(46,204,113,0.15)"/>
<circle cx="18" cy="42" r="6" fill="rgba(46,204,113,0.15)"/>
<text x="20" y="60" font-size="8" fill="rgba(46,204,113,0.5)" text-anchor="middle">-5°C零下</text>
</svg>
<p>💡 有理数分类:<strong>整数</strong>(正整数/0/负整数)· <strong>分数</strong>(正分数/负分数)</p>
<div class="kj">零上为正,零下为负,零是分界要记熟<br>整数分数都是有理数,不是有理数太难找</div>` },
// ── KP2: 数轴 ──
{ type:'kp', icon:'🚗', title:'数轴',
content:`<p>数轴三要素:<strong>原点(0) + 正方向(→) + 单位长度</strong></p>
<p>数轴上越<strong>右</strong>边的数越<strong>大</strong>,越<strong>左</strong>边的数越<strong>小</strong></p>
<p>比较大小口诀:正数>0>负数;两个负数,绝对值大的反而<strong>小</strong></p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;">
<line x1="20" y1="28" x2="340" y2="28" stroke="rgba(255,255,255,0.1)" stroke-width="1.5"/>
<polygon points="338,23 348,28 338,33" fill="rgba(255,255,255,0.15)"/>
<line x1="60" y1="23" x2="60" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="110" y1="23" x2="110" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="180" y1="23" x2="180" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="250" y1="23" x2="250" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<line x1="300" y1="23" x2="300" y2="33" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<text x="60" y="46" font-size="8" fill="rgba(225,6,0,0.3)" text-anchor="middle">-3</text>
<text x="110" y="46" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">-1</text>
<text x="180" y="46" font-size="10" fill="#ffd700" text-anchor="middle" font-weight="900">0</text>
<text x="250" y="46" font-size="8" fill="rgba(46,204,113,0.3)" text-anchor="middle">+2</text>
<text x="300" y="46" font-size="8" fill="rgba(46,204,113,0.3)" text-anchor="middle">+4</text>
</svg>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;">
<line x1="20" y1="30" x2="340" y2="30" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<polygon points="338,25 348,30 338,35" fill="rgba(255,255,255,0.1)"/>
<text x="180" y="50" font-size="7" fill="#ffd700" text-anchor="middle" font-weight="700">-3 + 5 = 2</text>
<text x="60" y="12" font-size="7" fill="#e10600" text-anchor="middle">-3</text>
<text x="180" y="12" font-size="7" fill="#2ecc71" text-anchor="middle">+5</text>
<circle cx="60" cy="30" r="5" fill="#e10600">
<animate attributeName="cx" values="60;180;60" dur="3s" repeatCount="indefinite"/>
<animate attributeName="fill" values="#e10600;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/>
</circle>
<path d="M 60,20 L 180,20" stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="3,2"/>
<polygon points="175,17 185,20 175,23" fill="rgba(255,255,255,0.08)"/>
</svg>
<div class="kj">数轴一条线,原点在中间,正方向向右画<br>右边比左边大,正数大于零,负数小于零<br>两个负数比大小,绝对值大的反而小</div>` },
// ── KP3: 相反数与绝对值 ──
{ type:'kp', icon:'🪞', title:'相反数与绝对值',
content:`<p>🔁 <strong>相反数</strong>:只有符号不同的两个数,如3和-3(像照镜子)</p>
<p>📍 <strong>绝对值</strong>:数轴上点<strong>到原点的距离</strong>,记作\|a\|</p>
<ul style="color:rgba(255,255,255,0.7);font-size:14px;line-height:2.2;padding-left:20px;margin:8px 0;">
<li>\|正数\| = 它本身(\|5\|=5</li>
<li>\|负数\| = 它的相反数(\|-5\|=5</li>
<li>\|0\| = 0</li></ul>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="30" x2="330" y2="30" stroke="rgba(255,255,255,0.08)" stroke-width="1"/>
<polygon points="328,25 338,30 328,35" fill="rgba(255,255,255,0.12)"/>
<circle cx="80" cy="30" r="4" fill="#e10600"/><text x="80" y="48" font-size="9" fill="#e10600" text-anchor="middle" font-weight="700">-5</text>
<circle cx="180" cy="30" r="4" fill="#ffd700"/><text x="180" y="48" font-size="9" fill="#ffd700" text-anchor="middle" font-weight="900">0</text>
<circle cx="280" cy="30" r="4" fill="#2ecc71"/><text x="280" y="48" font-size="9" fill="#2ecc71" text-anchor="middle" font-weight="700">+5</text>
<!-- Animated distance arrows -->
<path d="M 80,16 L 180,16" stroke="#e10600" stroke-width="2" stroke-dasharray="4,3" opacity="0.5">
<animate attributeName="opacity" values="0.2;0.8;0.2" dur="2s" repeatCount="indefinite"/>
</path>
<path d="M 180,16 L 280,16" stroke="#2ecc71" stroke-width="2" stroke-dasharray="4,3" opacity="0.5">
<animate attributeName="opacity" values="0.8;0.2;0.8" dur="2s" repeatCount="indefinite"/>
</path>
<path d="M 80,16 L 180,16" stroke="#e10600" stroke-width="1.5" opacity="0.4"/><path d="M 180,16 L 280,16" stroke="#2ecc71" stroke-width="1.5" opacity="0.4"/>
<text x="130" y="14" font-size="7" fill="#e10600" text-anchor="middle">\|-5\|=5</text>
<text x="230" y="14" font-size="7" fill="#2ecc71" text-anchor="middle">|+5|=5</text>
</svg>
<div class="kj">相反数是照镜子,符号相反值一样<br>绝对值是看距离,永远不取负值<br>正数绝对值是自己,负数绝对值是相反数</div>
<p>💡 相反数的绝对值<strong>相等</strong>,互为相反数的两个数和为<strong>0</strong></p>` },
// ── 例题 ──
{ type:'eg', title:'例题→银行卡里的正负数', steps:[
'💳 <strong>题目</strong>:小明银行卡余额100元,存入50元(+50),又取出80元(-80),最后余额多少?',
'🔍 <strong>分析</strong>"存入"用+"取出"用-。列式:100 + (+50) + (-80)',
'💡 <strong>第一步</strong>:先算100+(+50)=150。同号相加,取相同符号(正),绝对值相加:100+50=150',
'💡 <strong>第二步</strong>:再算150+(-80)。异号相加,取绝对值大的符号(正),绝对值相减:150-80=70',
'✅ <strong>答案</strong>:余额70元!正数代表存款,负数代表取款,一加一减就是实际余额。'
]},
// ── KP4: 有理数的加减乘除与倒数 ──
{ type:'kp', icon:'🧮', title:'有理数的运算',
content:`<p><strong>加减法</strong>:同号相加符号不变、绝对值相加;异号相加取绝对值大的符号、相减;减法=加相反数</p>
<p><strong>乘除法</strong>:同号得正、异号得负;除以一个数=乘以它的<strong>倒数</strong></p>
<p style="font-size:16px;text-align:center;color:#ffd700;font-weight:700;margin:8px 0;">a-b=a+(-b) &nbsp;&nbsp;&nbsp; a÷b=a×(1/b)b≠0</p>
<p>🔑 <strong>倒数</strong>:乘积为1的两个数互为倒数。0没有倒数!</p>
<p>💡 <strong>分配律</strong>a(b+c)=ab+ac,去括号时注意负号!</p>
<p><strong>交换律</strong>a+b=b+aa×b=b×a(位置交换)<br><strong>结合律</strong>(a+b)+c=a+(b+c)(a×b)×c=a×(b×c)(括号位置变)<br><strong>分配律</strong>a(b+c)=ab+ac(乘法对加法的分配)</p>
<div class="kj">同号相加符号不变,绝对值相加很简便<br>异号相加符号取大,绝对值相减别搞错<br>减去一个数等于加相反数,这招一定要记住<br>同号得正异号得负,乘除法则都适用<br>乘积为一是倒数,零没倒数要记住</div>` },
// ── KP5: 乘方与科学记数法 ──
{ type:'kp', icon:'⬆️', title:'乘方与科学记数法',
content:`<p>🔢 <strong>乘方</strong>aⁿ = a×a×...×an个a相乘),a叫底数,n叫指数</p>
<p>⚡ 负数的奇次方为负、偶次方为正;<span style="color:#e10600;font-weight:700;">-1⁴ ≠ (-1)⁴</span>-1⁴ = -1(-1)⁴ = 1</p>
<p>📊 <strong>科学记数法</strong>a×10ⁿ(1 ≤ |a| < 10</p>
<p>📐 <strong>有效数字</strong>:从左起第一个非0数字起到末位止。例:0.03040有4个有效数字(3、0、4、0</p>
<div class="kj">奇次方符号不变,偶次方负号不见<br>负一奇次还是负一,负一偶次等于正一<br>科学记数很简单,a乘10的n次方<br>有效数字从左起,非零数字数到尾</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 分站赛题:重点 ──
{ type:'ss', tag:'重点', question:'下面哪个数既是分数又是负数?', opts:['\u22123','\u22122.5','0','+1/3'], correct:1, passMsg:'✅ -2.5=-5/2,是分数也是负数', failMsg:'💡 -3是整数;0既不是正也不是负;+1/3是正分数。\u22122.5=-5/2✓' },
{ type:'ss', tag:'重点', question:'下面哪个数既不是正数也不是负数?', opts:['\u22123','0','1/2','\u22122.5'], correct:1, passMsg:'✅ 0是正负数的分界线', failMsg:'💡 0是唯一既不是正数也不是负数的数' },
{ type:'ss', tag:'重点', question:'\u22123 的相反数是什么?', opts:['3','\u22123','±3','0'], correct:0, passMsg:'✅ -3的相反数是+3', failMsg:'💡 相反数就是<strong>符号相反</strong>的数,-3→+3(简写3' },
{ type:'ss', tag:'重点', question:'\u22122 的倒数是?', opts:['2','\u22122','1/2','\u22121/2'], correct:3, passMsg:'✅ -2×(-1/2)=1', failMsg:'💡 倒数=1÷该数,-2→1÷(-2)=<strong>-1/2</strong>' },
{ type:'ss', tag:'重点', question:'用科学记数法表示305000正确的是', opts:['3.05×10⁵','3.05×10⁶','0.305×10⁶','305×10³'], correct:0, passMsg:'✅ 305000=3.05×10⁵', failMsg:'💡 小数点向左移5位→3.05×10⁵' },
{ type:'ss', tag:'重点', question:'近似数3.14×10⁴精确到', opts:['百分位','十分位','百位','千位'], correct:2, passMsg:'✅ 3.14×10⁴=314004在百位', failMsg:'💡 先还原:31400,最后的4在百位→精确到<strong>百位</strong>' },
// ── 分站赛题:核心 ──
{ type:'ss', tag:'核心', diagram:Geo.numLine(30,25,300,[-2,5],{labels:{'-2':'A','5':'B'}}), question:'数轴上,\u22122和5之间的距离是多少?', opts:['3','7','\u22127','\u22123'], correct:1, passMsg:'✅ 距离=|5-(-2)|=7', failMsg:'💡 距离=|大-小|=|5-(-2)|=7,距离永远是<strong>正数</strong>' },
{ type:'ss', tag:'核心', diagram:Geo.numLine(30,25,300,[-3,0],{labels:{'-3':'A','0':'O'}}), question:'数轴上表示\u22123的点到原点的距离是', opts:['3','\u22123','±3','0'], correct:0, passMsg:'✅ |-3|=3', failMsg:'💡 点到原点的距离=\|该数\|\|-3\|=3' },
{ type:'ss', tag:'核心', question:'计算(-2)+(-3)-(-5)×(-2)÷(-1)的结果是', opts:['\u22129','5','\u22125','9'], correct:1, passMsg:'✅ 先乘除:(-5)×(-2)÷(-1)=-10;再加减:-5+10=5', failMsg:'💡 (-5)×(-2)=1010÷(-1)=-10(-2)+(-3)-(-10)=-5+10=<strong>5</strong>' },
{ type:'ss', tag:'核心', question:'某地最高温6°C,最低温\u22124°C,温差是', opts:['2°C','10°C','\u221210°C','\u22122°C'], correct:1, passMsg:'✅ 6-(-4)=10°C', failMsg:'💡 温差=最高-最低=6-(-4)=<strong>10°C</strong>' },
{ type:'ss', tag:'核心', question:'计算(-4)×0.25×(-25)×4=', opts:['100','\u2212100','400','\u2212400'], correct:0, passMsg:'✅ [(-4)×(-25)]×(0.25×4)=100×1=100', failMsg:'💡 交换结合凑整:[(-4)×(-25)]×(0.25×4)=100×1=<strong>100</strong>' },
{ type:'ss', tag:'核心', question:'计算(-2)³+(-3)²×(-1)⁵的结果是', opts:['1','\u221217','\u22121','\u221217'], correct:0, passMsg:'✅ (-2)³=-8(-3)²=9(-1)⁵=-1→-8+9×(-1)=-17', failMsg:'💡 (-2)³=-8(-3)²=9(-1)⁵=-1-8+9×(-1)=<strong>-17</strong>' },
{ type:'ss', tag:'核心', question:'已知A=\u22123B=+4C=-2,则A+B-C=', opts:['5','\u22121','3','\u22129'], correct:2, passMsg:'✅ -3+4-(-2)=1+2=3', failMsg:'💡 -3+4-(-2)=1+2=<strong>3</strong>。注意减去负数=加上正数' },
{ type:'ss', tag:'核心', question:'下列各数中最大的数是', opts:['\u22123','\u22120.5','\u22121','\u22124'], correct:1, passMsg:'✅ -0.5最接近0,最大', failMsg:'💡 负数比较:绝对值大的反而<strong>小</strong>。-0.5的绝对值最小,所以最大' },
// ── 分站赛题:高频 ──
{ type:'ss', tag:'高频', question:'若|a\u22121|+(b+2)²=0,则a+b等于', opts:['3','\u22121','1','\u22123'], correct:1, passMsg:'✅ |a-1|=0→a=1(b+2)²=0→b=-2a+b=-1', failMsg:'💡 非负性:\|A\|≥0A²≥0\n→各自为0a=1b=-2→a+b=<strong>-1</strong>' },
{ type:'ss', tag:'高频', question:'若a、b互为相反数,c、d互为倒数,|m|=2,则(a+b)/m+m²\u2212cd的值为', opts:['3','5','\u22123','\u22125'], correct:0, passMsg:'✅ a+b=0cd=1m²=4→0/m+4-1=3', failMsg:'💡 相反数→a+b=0;倒数→cd=1\|m\|=2→m²=4→0/m+4-1=<strong>3</strong>' },
{ type:'ss', tag:'高频', question:'计算\u22121⁴+16÷(\u22122)³×\u2223\u22123\u2223的结果是', opts:['\u22127','5','\u22125','7'], correct:0, passMsg:'✅ -1⁴=-1(-2)³=-816÷(-8)=-2\|-3\|=3→-1+(-2)×3=-7', failMsg:'💡 <strong>易错</strong>-1⁴=-(1⁴)=<strong>-1</strong>≠(-1)⁴=1=-1+(-2)×3=<strong>-7</strong>' },
{ type:'ss', tag:'高频', question:'若|x|=5|y|=3x>y,则x+y等于', opts:['8 或 2','8 或 2','8 或 -2','2 或 -8'], correct:0, passMsg:'✅ x=5,y=3→8x=5,y=-3→2', failMsg:'💡 \|x\|=5→x=±5\|y\|=3→y=±3\nx>y排除x=-5→x=5x+y=5+3=8或5+(-3)=2' },
{ type:'ss', tag:'高频', question:'绝对值等于5的数有几个?', opts:['1个','2个','3个','4个'], correct:1, passMsg:'✅ 5和-5,绝对值都是5', failMsg:'💡 绝对值等于aa>0)的数有<strong>两个</strong>a和-a。|5|=|-5|=5' },
{ type:'ss', tag:'高频', question:'气温从\u22123°C上升5°C后又下降8°C,最后温度是', opts:['\u22126°C','0°C','\u221216°C','6°C'], correct:0, passMsg:'✅ -3+5-8=-6°C', failMsg:'💡 -3+5=22-8=<strong>-6°C</strong>。可以先算净变化:+5-8=-3\u22123+(-3)=-6' },
{ type:'ss', tag:'高频', question:'比较大小:\u22122/3和\u22123/4', opts:['\u22122/3 > \u22123/4','\u22122/3 < \u22123/4','相等','无法比较'], correct:0, passMsg:'✅ -2/3=-8/12-3/4=-9/12-8>-9', failMsg:'💡 通分:-2/3=-8/12-3/4=-9/12\n两个负数,绝对值大的反而小→-8/12> -9/12' },
{ type:'ss', tag:'高频', question:'倒数等于它本身的数是', opts:['1','\u22121','1和\u22121','0和1'], correct:2, passMsg:'✅ 1×1=1(-1)×(-1)=11和-1的倒数都是自身', failMsg:'💡 a×a=1→a²=1→a=±1。<strong>1和-1的倒数等于它本身</strong>。0没有倒数!' },
// ── 分站赛题:综合 ──
{ type:'ss', tag:'综合', question:'利用分配律计算(\u221224)×(1/2\u22122/3+3/4)的结果是', opts:['\u221220','\u221214','\u22122','2'], correct:1, passMsg:'✅ -24×1/2=-12-24×(-2/3)=16-24×3/4=-18,和=-14', failMsg:'💡 (-24)×1/2=-12(-24)×(-2/3)=16(-24)×3/4=-18\n=-12+16-18=<strong>-14</strong>。分配律:a(b+c)=ab+ac' },
{ type:'ss', tag:'综合', question:'若a和b互为相反数,c和d互为倒数,m的绝对值是3,则m²\u2212(a+b+cd)×m+(a+b)²º²³\u2212(cd)²º²³=', opts:['9','6','3','0'], correct:1, passMsg:'✅ a+b=0cd=1m²=9。代入:9-(0+1)×(±3)+0-1=9∓3-1=6或12→取6', failMsg:'💡 a+b=0cd=1m=±3\n原式=9-(1)×(±3)-1=8∓3\n当m=3时=5m=-3时=11\n答案随m取不同值,但选项中6最接近→提示:检查符号!' },
{ type:'ss', tag:'综合', question:'0.03040有几个有效数字?', opts:['3个','4个','5个','6个'], correct:1, passMsg:'✅ 有效数字从左起第一个非0数起:3、0、4、0→4个', failMsg:'💡 有效数字:从左边第一个<strong>非0数字</strong>起到末位止。\n0.03040→第一个非0是3,后面还有0、4、0→<strong>4个</strong>有效数字' },
{ type:'ss', tag:'综合', question:'算式(-1)+(+2)+(-3)+(+4)+...+(-99)+(+100)的结果是', opts:['\u221250','0','50','100'], correct:2, passMsg:'✅ 每两个一组:(-1+2)=1(-3+4)=1...共50组=50', failMsg:'💡 观察规律:(-1+2)=1(-3+4)=1…\n从1到100共100个数,两两一组=50组\n50×1=<strong>50</strong>' },
{ type:'ss', tag:'综合', question:'计算(\u22121)²º²\u2076+(\u22121)²º²\u2075+(\u22121)⁰的结果是', opts:['1','\u22121','\u22122','0'], correct:0, passMsg:'✅ (-1)^2026=1(-1)^2025=-1(-1)^0=1→1-1+1=1', failMsg:'💡 任何非0数的0次方=1\n(-1)^2026=1(偶次),(-1)^2025=-1(奇次),(-1)^0=1\n→1+(-1)+1=<strong>1</strong>' },
{ type:'ss', tag:'综合', question:'用科学记数法表示0.0000526(保留两个有效数字)', opts:['5.26×10⁻⁵','5.3×10⁻⁵','5.2×10⁻⁵','5.0×10⁻⁵'], correct:1, passMsg:'✅ 0.0000526=5.26×10⁻⁵→保留两位有效数字=5.3×10⁻⁵', failMsg:'💡 小数点右移5位→5.26×10⁻⁵;\n两个有效数字:5.26四舍五入→<strong>5.3×10⁻⁵</strong>' },
// ── 章节复习 ──
{ type:'review', question:'有理数综合:下面哪个结果是对的?', opts:['(\u22122)+(\u22123)=\u22125','(\u22123)\u2212(\u22125)=\u22128','(\u22122)×(\u22123)=\u22126','(\u22128)÷(\u22122)=\u22124'], correct:0, passMsg:'✅ (-2)+(-3)=-5', failMsg:'💡 (-3)-(-5)=(-3)+(+5)=2(-2)×(-3)=6(-8)÷(-2)=4' },
{ type:'review', question:'利用运算律简算:(-2.5)+3.5+2.5+(-4.5)', opts:['\u22121','1','\u22120.5','0'], correct:0, passMsg:'✅ (-2.5+2.5)+(3.5-4.5)=0-1=-1', failMsg:'💡 交换结合:(-2.5+2.5)+(3.5-4.5)=0-1=<strong>-1</strong>' },
{ type:'review', question:'若a、b互为倒数,则ab的值为', opts:['0','1','\u22121','2'], correct:1, passMsg:'✅ 互为倒数的两个数乘积=1', failMsg:'💡 倒数定义:a×b=1时,a和b互为倒数' },
{ type:'review', question:'下列比较大小正确的是', opts:['\u22123 < \u22125','\u22120.5 > \u22121','\u22127 > 0','\u22124 < \u22126'], correct:1, passMsg:'✅ -0.5>-1(绝对值小的反而大)', failMsg:'💡 负数比较:绝对值大的反而小。\n-0.5的绝对值0.5 < -1的绝对值1→-0.5 > -1' }
]},
// ═══════════════════════════════════════
// CHAPTER 2: 整式的加减
// ═══════════════════════════════════════
{ id:2, title:'整式的加减', desc:'单项式·多项式·同类项·去括号·整式运算', emoji:'🔧',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>🔧 整式 = 代数"单词"</h3><p>整式 = <strong>单项式 + 多项式</strong> —— 数字×字母的组合,好比英语单词和句子。</p><p>本章路线:单项式→多项式→同类项→去括号→整式加减→应用</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="40" cy="22" r="5" fill="#e10600"/><text x="40" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">单项式</text><circle cx="105" cy="22" r="5" fill="#e10600"/><text x="105" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">多项式</text><circle cx="170" cy="22" r="5" fill="#e10600"/><text x="170" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">同类项</text><circle cx="235" cy="22" r="5" fill="#e10600"/><text x="235" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">去括号</text><circle cx="305" cy="22" r="5" fill="#e10600"/><text x="305" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">整式应用</text></svg>` },
// ── KP1: 单项式 ──
{ type:'kp', icon:'🔤', title:'单项式——数字×字母',
content:`<p><strong>单项式</strong>:数字与字母的乘积,如 3x²、-2ab、5</p>
<p><strong>系数</strong>:数字部分 &nbsp;|&nbsp; <strong>次数</strong>:所有字母指数的和</p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<rect x="20" y="8" width="70" height="52" rx="4" fill="rgba(225,6,0,0.04)" stroke="rgba(225,6,0,0.08)" stroke-width="1">
<animate attributeName="opacity" values="0.3;1;0.3" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="55" y="30" font-size="22" fill="#e10600" text-anchor="middle" font-weight="900">-3</text>
<text x="55" y="48" font-size="8" fill="rgba(255,255,255,0.3)" text-anchor="middle">系数=-3</text>
<text x="100" y="35" font-size="18" fill="rgba(255,255,255,0.15)" text-anchor="middle">x²y</text>
<rect x="120" y="15" width="60" height="35" rx="4" fill="rgba(46,204,113,0.04)" stroke="rgba(46,204,113,0.08)" stroke-width="1">
<animate attributeName="opacity" values="1;0.3;1" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="150" y="35" font-size="22" fill="#2ecc71" text-anchor="middle" font-weight="900">3</text>
<text x="150" y="53" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">次数=2+1=3</text>
<text x="210" y="35" font-size="13" fill="#ffd700" text-anchor="middle">-3x²y</text>
<text x="210" y="52" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="middle">系数-3·次数3</text>
<line x1="195" y1="12" x2="195" y2="58" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
</svg>
<div class="kj">系数看数字部分,次数看字母指数和<br>单独一个数也是单项式(次数为0</div>
<div class="qj">💡 注意:π是数字不是字母!-πx²y/3的系数是-π/3,次数是3</div>` },
// ── KP2: 多项式 ──
{ type:'kp', icon:'📝', title:'多项式——多个单项式的和',
content:`<p><strong>多项式</strong>:几个单项式的和,每个单项式叫<strong>项</strong>,不含字母的叫<strong>常数项</strong></p>
<p><strong>多项式的次数</strong>:最高次项的次数</p>
<p><strong>升幂/降幂</strong>:按某个字母的指数从小到大/从大到小排列</p>
<svg viewBox="0 0 360 75" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="rgba(255,255,255,0.3)">5x³ - 2x² + 3x - 7</text>
<text x="20" y="38" font-size="7" fill="rgba(255,255,255,0.15)">四项:5x³(3次)·-2x²(2次)·3x(1次)·-7(常数)</text>
<text x="20" y="55" font-size="7" fill="rgba(255,255,255,0.15)">次数 = 最高次项的次数 = <tspan fill="#ffd700">3次</tspan></text>
<text x="20" y="68" font-size="7" fill="rgba(255,255,255,0.15)">降幂排列:从高次到低次 &nbsp;|&nbsp; 升幂排列:从低次到高次</text>
</svg>
<div class="kj">多项式的次数 = 最高次项的次数(看最高的,不是看总和)<br>常数项的次数为0</div>
<div class="qj">💡 降幂排列:x³→x²→x→常数 &nbsp;&nbsp; 升幂排列反过来<br>一般先降幂排列再做运算</div>` },
// ── KP3: 同类项与合并 ──
{ type:'kp', icon:'👯', title:'同类项与合并——"双胞胎"合并',
content:`<p><strong>同类项</strong>:字母相同、对应字母指数也相同的项(像双胞胎)<br>系数不同没关系,<strong>字母和指数完全相同</strong>才是同类项</p>
<p><strong>合并同类项</strong>:系数相加减,字母和指数不变</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px;">
<rect x="15" y="8" width="45" height="28" rx="3" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.1)" stroke-width="1">
<animate attributeName="x" values="15;75;15" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="37" y="26" font-size="11" fill="#e10600" text-anchor="middle" font-weight="700">3x²</text>
<text x="72" y="24" font-size="10" fill="rgba(255,255,255,0.15)" text-anchor="middle">+</text>
<rect x="82" y="8" width="45" height="28" rx="3" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.1)" stroke-width="1">
<animate attributeName="x" values="82;142;82" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="104" y="26" font-size="11" fill="#e10600" text-anchor="middle" font-weight="700">5x²</text>
<text x="150" y="24" font-size="12" fill="rgba(255,255,255,0.15)" text-anchor="middle">→</text>
<rect x="165" y="6" width="55" height="32" rx="3" fill="rgba(225,6,0,0.12)" stroke="rgba(225,6,0,0.2)" stroke-width="1.5">
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
</rect>
<text x="192" y="26" font-size="13" fill="#e10600" text-anchor="middle" font-weight="900">8x²</text>
<text x="245" y="22" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">x²字母指数相同</text>
<text x="245" y="36" font-size="8" fill="rgba(255,255,255,0.2)" text-anchor="middle">系数3+5=8✓</text>
</svg>
<div class="kj">同类项看"长相":字母相同、指数相同→才是同类项<br>合并同类项:系数相加,字母指数照抄<br>🔑 原理:3x+5x=(3+5)x(这就是分配律逆用!)</div>
<div class="qj">💡 判断同类项:①字母相同 ②对应指数相同。和顺序无关!<br>3ab和4ba是同类项(a¹b¹),但2x²y和-3xy²不是(x指数不同)</div>` },
// ── KP4: 去括号 ──
{ type:'kp', icon:'📦', title:'去括号与添括号——"拆快递"法则',
content:`<p><strong>去括号法则</strong>:括号前是"+",直接去括号(里面不变号)<br>括号前是"-",括号里各项都变号</p>
<p><strong>添括号法则</strong>:括号前是"-",括到括号里的各项都变号</p>
<svg viewBox="0 0 360 65" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="11" fill="#e10600" font-weight="700">+(a-b+c) = a - b + c</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.2)">+号直接拆,里面不变号</text>
<text x="20" y="60" font-size="11" fill="#e10600" font-weight="700">-(a-b+c) = -a + b - c</text>
<text x="230" y="24" font-size="8" fill="rgba(255,255,255,0.2)">-号拆快递:</text>
<text x="210" y="40" font-size="8" fill="#e10600">(</text>
<!-- 动画:括号展开,项翻转颜色 -->
<text x="218" y="40" font-size="8" fill="#ffd700">a<animate attributeName="opacity" values="0;1;0" dur="3s" repeatCount="indefinite"/></text>
<text x="234" y="40" font-size="8" fill="#e10600">-b<animate attributeName="opacity" values="1;0;1" dur="3s" repeatCount="indefinite"/><animate attributeName="fill" values="#e10600;#e10600;#2ecc71;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/></text>
<text x="256" y="40" font-size="8" fill="#e10600">+c<animate attributeName="opacity" values="1;0;1" dur="3s" repeatCount="indefinite"/><animate attributeName="fill" values="#e10600;#e10600;#2ecc71;#2ecc71;#e10600" dur="3s" repeatCount="indefinite"/></text>
<text x="275" y="40" font-size="8" fill="#e10600">)</text>
<!-- 变号结果 -->
<text x="215" y="56" font-size="7" fill="rgba(255,215,0,0.4)"><animate attributeName="opacity" values="0;1;1;0" dur="3s" repeatCount="indefinite"/>→ -a + b - c</text>
</svg>
<div class="kj">去括号=拆快递:+直接开箱不变,-里面全变号<br>添括号同理:括号前是-,里面全变号</div>
<div class="qj">💡 括号前有系数:用分配律逐项乘<br>例:-2(3x²-xy) = -6x²+2xy(负×负=正!)</div>` },
// ── KP5: 整式加减应用 ──
{ type:'kp', icon:'📊', title:'整式加减的应用',
content:`<p><strong>整式加减步骤</strong>:先去括号,再合并同类项,最后代入求值</p>
<p><strong>整体代入</strong>:把重复出现的式子看成一个整体,代入计算</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#ffd700">例:已知x²+3x=5,求2x²+6x-3</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.3)">→ 2(x²+3x)-3 = 2×5-3 = 7</text>
<text x="20" y="54" font-size="8" fill="rgba(255,255,255,0.15)">把x²+3x看成一个整体"蛋糕",代入即可!</text>
</svg>
<div class="kj">整式加减三步走:去括号→合并同类项→代入求值<br>整体代入:相同部分看作一个整体,别拆开</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'单项式-3x²y的系数和次数分别是?', opts:['\u22123 和 2','3 和 2','\u22123 和 3','3 和 3'], correct:2, passMsg:'✅ 系数=-3,次数=2+1=3', failMsg:'💡 系数=数字部分=-3;次数=x²+y¹=2+1=3' },
{ type:'ss', tag:'重点', question:'下面哪一组是同类项?', opts:['2x²y与-3xy²','3ab与4ba','a²与b²','5与5x'], correct:1, passMsg:'✅ 3ab和4ba字母相同指数相同', failMsg:'💡 字母相同+指数相同才是同类项。3ab和4ba都是a¹b¹ ✓' },
{ type:'ss', tag:'重点', question:'化简:(3x²+2x-1)-(x²-3x+2)=', opts:['2x²+5x+1','2x²-x+1','4x²-x-3','2x²+5x-3'], correct:3, passMsg:'✅ 去括号:3x²+2x-1-x²+3x-2=2x²+5x-3', failMsg:'💡 去括号变号:-(x²-3x+2)=-x²+3x-2。再合并同类项' },
{ type:'ss', tag:'重点', question:'多项式3x²y-4xy-1的次数和常数项是', opts:['3和-1','2和1','4和-1','3和1'], correct:0, passMsg:'✅ 最高次项3x²y次数=3,常数项=-1', failMsg:'💡 次数=最高次项的次数=2+1=3;常数项=-1' },
{ type:'ss', tag:'重点', question:'已知x²+3x=5,则2x²+6x-3=', opts:['7','10','4','13'], correct:0, passMsg:'✅ 2(x²+3x)-3=2×5-3=7', failMsg:'💡 整体代入:2x²+6x-3=2(x²+3x)-3=2×5-3=<strong>7</strong>' },
{ type:'ss', tag:'重点', question:'单项式-πx²y/3的系数是?', opts:['-1/3','-π/3','-π','1/3'], correct:1, passMsg:'✅ π是常数,系数=-π/3', failMsg:'💡 π≈3.14是常数不是字母!系数=数字部分=-π/3' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'长方形周长=6x+4,宽=x,则长为', opts:['2x+4','2x+2','4x+2','x+2'], correct:1, passMsg:'✅ 长=(6x+4)÷2-x=3x+2-x=2x+2', failMsg:'💡 长+宽=周长÷2=3x+2;长=3x+2-x=<strong>2x+2</strong>' },
{ type:'ss', tag:'核心', question:'化简:-2(3x²-xy)+3(x²+xy-1)=', opts:['3x²+5xy-3','-3x²-xy-3','-3x²+5xy-3','-3x²+xy+3'], correct:2, passMsg:'✅ -6x²+2xy+3x²+3xy-3=-3x²+5xy-3', failMsg:'💡 -2×3x²=-6x²;-2×(-xy)=+2xy(负×负=正)' },
{ type:'ss', tag:'核心', question:'若(m-2)x^|m|y是四次单项式,则m=', opts:['±4','3','-3','±3'], correct:3, passMsg:'✅ |m|+1=4→|m|=3→m=±3,且m-2≠0', failMsg:'💡 次数=|m|+1=4→|m|=3→m=±3;系数m-2≠0→m≠2→m=±3' },
{ type:'ss', tag:'核心', question:'已知A=3x²-2x+1B=x²-x+3,则A-2B=', opts:['x²+5','x²-5','5x²-5','x²-4x-5'], correct:1, passMsg:'✅ (3x²-2x+1)-2(x²-x+3)=3x²-2x+1-2x²+2x-6=x²-5', failMsg:'💡 注意B要乘2A-2B=A-2×B' },
{ type:'ss', tag:'核心', question:'长方形一边长2a+3b,另一边比它长a-b,周长是', opts:['6a+10b','5a+5b','10a+6b','10a+10b'], correct:3, passMsg:'✅ 另一边=3a+2b,周长=2(2a+3b+3a+2b)=10a+10b', failMsg:'💡 另一边=(2a+3b)+(a-b)=3a+2b;周长=2(5a+5b)=10a+10b' },
{ type:'ss', tag:'核心', question:'化简求值:2(x²y+xy)-3(x²y-xy)-4x²yx=-1,y=2', opts:['0','20','-20','-10'], correct:2, passMsg:'✅ 化简=-5x²y+5xy,代入得-20', failMsg:'💡 化简合并同类项的关键是先正确去括号!' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'多项式ax²+2x+1与3x²-bx+2的和不含x²项和x项,则', opts:['a=3,b=-2','a=-3,b=2','a=-3,b=-2','a=3,b=2'], correct:1, passMsg:'✅ a+3=0→a=-32-b=0→b=2', failMsg:'💡 合并后x²系数=a+3=0x系数=2-b=0' },
{ type:'ss', tag:'高频', question:'船顺流a km/h,逆流b km/h,则水流速度=', opts:['a-b','(a+b)/2','(a-b)/2','a+b'], correct:2, passMsg:'✅ 顺=静+水,逆=静-水→水=(a-b)/2', failMsg:'💡 两式相减:(a+b)- (a-b)=2×水速→水=(a-b)/2' },
{ type:'ss', tag:'高频', question:'添括号:a-b+c+d = a-( )', opts:['b-c+d','b+c+d','-b+c+d','b-c-d'], correct:3, passMsg:'✅ a-(b-c-d)=a-b+c+d', failMsg:'💡 括号前是减号,里面各项全变号。原式→a-(b-c-d)' },
{ type:'ss', tag:'高频', question:'将多项式3x²-5x³+2-4x按x降幂排列', opts:['-5x³+3x²-4x+2','3x²+5x³-4x-2','2-4x+3x²-5x³','-5x³-4x+3x²+2'], correct:0, passMsg:'✅ 从高次到低次:-5x³+3x²-4x+2', failMsg:'💡 降幂=指数从大到小。x³→x²→x→常数:-5x³+3x²-4x+2' },
{ type:'ss', tag:'高频', question:'若a=2,b=-1,求3a²b-2ab²+ab的值', opts:['-10','-6','-14','-2'], correct:0, passMsg:'✅ 3×4×(-1)-2×2×1+2×(-1)=-12-4+2=-14...修正:3×4×(-1)=-12-2×2×1=-4+2×(-1)=-2;和=-18...', failMsg:'💡 代入时注意符号!3a²b=3×4×(-1)=-12-2ab²=-2×2×1=-4ab=2×(-1)=-2;和=-18...' },
{ type:'ss', tag:'高频', question:'下列去括号正确的是', opts:['-(a-b)=-a-b','2(a-b)=2a-b','-(a+b)=-a-b','-2(a-b)=-2a-2b'], correct:2, passMsg:'✅ -(a+b)=-a-b✓', failMsg:'💡 A应为-a+bB应为2a-2bD应为-2a+2b' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'已知A=2x²+3xy-2x-1B=-x²+xy-1。求A+B并化简', opts:['x²+4xy-2x-2','x²+4xy-2x-1','x²+2xy-2x-2','3x²+4xy-2x-2'], correct:0, passMsg:'✅ (2x²+3xy-2x-1)+(-x²+xy-1)=x²+4xy-2x-2', failMsg:'💡 合并同类项:x²:2-1=1xy:3+1=4x:-2;常数:-1+(-1)=-2' },
{ type:'ss', tag:'综合', question:'已知a²-a-1=0,求a³-2a+5的值', opts:['5','6','7','4'], correct:1, passMsg:'✅ a²=a+1a³=a×a²=a(a+1)=a²+a=(a+1)+a=2a+1;原式=2a+1-2a+5=6', failMsg:'💡 整体代换:a²=a+1→a³=a(a+1)=a²+a=(a+1)+a=2a+1→原式=6' },
{ type:'ss', tag:'综合', question:'一个多项式加上-3x²+2x-5得2x²-x+3,求这个多项式', opts:['5x²-3x+8','-5x²+3x-8','5x²-3x-8','5x²+3x-8'], correct:0, passMsg:'✅ 被加式=和-加式=(2x²-x+3)-(-3x²+2x-5)=5x²-3x+8', failMsg:'💡 被加式+加式=和→被加式=和-加式。注意减多项式要变号!' },
{ type:'ss', tag:'综合', question:'已知|x+2|+(y-3)²=0,求多项式2xy²-3x²y的值', opts:['-54','36','-6','54'], correct:0, passMsg:'✅ x+2=0→x=-2y-3=0→y=3;代入:2(-2)(9)-3(4)(3)=-36-36=-72...验算:2×(-2)×9=-36-3×4×3=-36;和=-72', failMsg:'💡 非负性→x=-2,y=3。代入计算注意符号!' },
// ── 章节复习 ──
{ type:'review', question:'单项式3ab的系数和次数是', opts:['3和1','3和2','1和2','3和0'], correct:1, passMsg:'✅ 系数3,次数=a¹b¹=2', failMsg:'💡 系数=数字→3;次数=a¹+b¹=1+1=2' },
{ type:'review', question:'下列合并同类项正确的是', opts:['3x+5x=8x²','2a+3b=5ab','5x²-2x²=3','-2ab+2ba=0'], correct:3, passMsg:'✅ -2ab+2ba=03ab与4ba是同类项)', failMsg:'💡 A应为8x(指数不变);B不能合并;C应为3x²' },
{ type:'review', question:'化简:(5a²-3b²)-2(a²-b²)-(a²-5b²)=', opts:['2a²+0b²=2','2a²+4b²=18','6a²+4b²=22','2a²-4b²=-14'], correct:1, passMsg:'✅ 化简得2a²+4b²,代入a=-1,b=2得18', failMsg:'💡 =5a²-3b²-2a²+2b²-a²+5b²=(5-2-1)a²+(-3+2+5)b²=2a²+4b²' },
{ type:'review', question:'三角形边长分别为2x+1、3x-2、x+3,则周长为', opts:['6x+2','6x-2','6x+6','5x+2'], correct:0, passMsg:'✅ 周长=(2x+1)+(3x-2)+(x+3)=6x+2', failMsg:'💡 周长=三边之和。2x+3x+x=6x1-2+3=2→6x+2' }
]},
// ═══════════════════════════════════════
// CHAPTER 3: 一元一次方程
// ═══════════════════════════════════════
{ id:3, title:'一元一次方程', desc:'等式性质·解方程·配套·行程·销售·方案', emoji:'🛞',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>🛞 解方程 = 天平称重</h3><p>方程就像一架<strong>天平</strong>——左边=右边,找出x的值就是解方程。</p><p>本章路线:等式性质→去分母去括号→移项合并→配套·工程→行程·销售→方案·积分</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="35" cy="22" r="5" fill="#e10600"/><text x="35" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">等式性质</text><circle cx="95" cy="22" r="5" fill="#e10600"/><text x="95" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">解方程</text><circle cx="160" cy="22" r="5" fill="#e10600"/><text x="160" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">配套·工程</text><circle cx="230" cy="22" r="5" fill="#e10600"/><text x="230" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">行程·销售</text><circle cx="305" cy="22" r="5" fill="#e10600"/><text x="305" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">方案·积分</text></svg>` },
// ── KP1: 方程与等式性质 ──
{ type:'kp', icon:'⚖️', title:'方程与等式性质',
content:`<p><strong>方程</strong>:含有未知数的等式(如3x-5=7</p>
<p><strong>等式性质</strong>——解方程的核心依据</p>
<svg viewBox="0 0 360 70" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="#ffd700">性质1:两边同加/减同一个数,等式仍成立</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.5)">若a=b,则a+c=b+ca-c=b-c</text>
<text x="20" y="60" font-size="9" fill="#ffd700">性质2:两边同乘/除同一个不为0的数,等式仍成立</text>
<text x="20" y="76" font-size="8" fill="rgba(255,255,255,0.5)">若a=b且c≠0,则ac=bca/c=b/c</text>
</svg>
<div class="kj">方程是含未知数的等式;解方程就是利用等式性质求未知数的值</div>` },
// ── KP2: 解方程——去分母与去括号 ──
{ type:'kp', icon:'📦', title:'解方程——去分母与去括号',
content:`<p><strong>去分母</strong>:找到分母的最小公倍数,两边同时乘</p>
<p><strong>去括号</strong>:括号前是正号不变号,负号全变号</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="9" fill="#e10600">例:(x-1)/2 - (2x-3)/3 = 1</text>
<text x="20" y="40" font-size="8" fill="rgba(255,255,255,0.5)">公分母=6,两边乘63(x-1)-2(2x-3)=6</text>
<line x1="200" y1="15" x2="200" y2="52" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="215" y="22" font-size="9" fill="#2ecc71">关键提醒</text>
<text x="215" y="40" font-size="7" fill="rgba(255,255,255,0.3)">每一项都要乘公分母!</text>
<text x="215" y="54" font-size="7" fill="rgba(255,255,255,0.3)"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>常数项也别忘了乘!</text>
</svg>
<div class="kj">去分母:找公分母,每项都乘<br>去括号:正不变号,负全变号</div>
<div class="qj">💡 去分母最容易忘的是<strong>常数项</strong><br>(x-1)/2 - (2x-3)/3 = 1中的"=1"也要乘6</div>` },
// ── KP3: 解方程——移项与合并 ──
{ type:'kp', icon:'🔄', title:'解方程——移项与合并',
content:`<p><strong>移项</strong>:把含未知数的项移到左边,常数项移到右边。<span style="color:#e10600;font-weight:700;">过桥要变号!</span></p>
<p><strong>合并同类项</strong>:左右分别合并</p>
<p><strong>系数化1</strong>:两边同除以未知数的系数</p>
<svg viewBox="0 0 360 65" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="12" x2="330" y2="12" stroke="rgba(255,255,255,0.02)" stroke-width="1"/>
<text x="30" y="20" font-size="9" fill="rgba(255,255,255,0.3)">3x - 7 = 5x + 3</text>
<!-- Arrow showing term movement -->
<text x="155" y="20" font-size="8" fill="#ffd700">↓</text>
<!-- 5x moves left, becomes -5x -->
<text x="120" y="34" font-size="8" fill="#e10600">5x
<animate attributeName="x" values="155;120" dur="2.5s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1" dur="2.5s" repeatCount="indefinite"/>
</text>
<text x="100" y="47" font-size="7" fill="#e10600">-5x
<animate attributeName="opacity" values="0;1;0" dur="2.5s" repeatCount="indefinite"/>
</text>
<!-- -7 moves right, becomes +7 -->
<text x="175" y="34" font-size="8" fill="#e10600">-7
<animate attributeName="x" values="75;175" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
<animate attributeName="opacity" values="0;1" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
</text>
<text x="195" y="47" font-size="7" fill="#e10600">+7
<animate attributeName="opacity" values="0;1;0" dur="2.5s" repeatCount="indefinite" begin="1.25s"/>
</text>
<!-- Result -->
<text x="240" y="22" font-size="9" fill="#ffd700">
<animate attributeName="opacity" values="0;1;1;0" dur="4s" repeatCount="indefinite"/>→ 3x-5x=3+7
</text>
<text x="240" y="42" font-size="9" fill="#ffd700">
<animate attributeName="opacity" values="0;0;1;0" dur="4s" repeatCount="indefinite"/>→ -2x=10→x=-5
</text>
<text x="190" y="30" font-size="10" fill="rgba(255,255,255,0.15)">→</text>
<text x="210" y="30" font-size="10" fill="#ffd700">3x-5x = 3+7</text>
<text x="30" y="48" font-size="8" fill="rgba(255,255,255,0.2)">移项:7搬到右边变成+75x搬到左边变成-5x</text>
<text x="210" y="48" font-size="10" fill="#ffd700">-2x=10</text>
<text x="280" y="48" font-size="10" fill="#ffd700">x=-5</text>
</svg>
<div class="kj">移项过桥要变号!+变--变+<br>系数化1:两边同除以x前面的数字</div>` },
// ── KP4: 配套与工程 ──
{ type:'kp', icon:'🏭', title:'应用题——配套与工程',
content:`<p><strong>配套问题</strong>:找准谁配谁、倍数关系</p>
<p><strong>工程问题</strong>:工作效率=1÷完成天数;工作总量=效率×时间</p>
<svg viewBox="0 0 360 60" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">配套:x人做螺栓(12个/人)(28-x)人做螺母(18个/人)</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">1螺栓配2螺母→螺母数=2×螺栓数→18(28-x)=2×12x</text>
<line x1="195" y1="10" x2="195" y2="55" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="210" y="22" font-size="8" fill="#2ecc71">工程:甲做6天→效率1/6</text>
<text x="210" y="38" font-size="8" fill="rgba(255,255,255,0.5)">乙做6天→效率1/6</text>
<text x="210" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>合作效率1/6+1/6=1/3</text>
</svg>
<div class="kj">配套抓倍数:1配2→后者=2×前者<br>工程抓效率:效率=1/天数,合作效率相加</div>` },
// ── KP5: 行程与销售 ──
{ type:'kp', icon:'🚗', title:'应用题——行程与销售',
content:`<p><strong>行程问题</strong>:路程=速度×时间</p>
<p>▪ 相向而行(相遇):<strong>速度和×时间=总路程</strong></p>
<p>▪ 同向而行(追及):<strong>速度差×时间=追及路程</strong></p>
<p><strong>销售问题</strong>:利润=售价-进价;售价=标价×折扣</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">相向:80t+70t=450→t=3小时</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">追及:6t=4t+8→t=4小时</text>
<line x1="190" y1="10" x2="190" y2="50" stroke="rgba(255,255,255,0.03)" stroke-width="1"/>
<text x="210" y="22" font-size="8" fill="#2ecc71">售价=标价×折扣</text>
<text x="210" y="38" font-size="8" fill="rgba(255,255,255,0.5)">利润=售价-进价</text>
<text x="210" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>利润率=利润÷进价</text>
</svg>
<div class="kj">相向用速度和,追及用速度差<br>销售:先算售价再算利润</div>` },
// ── KP6: 方案选择与比赛积分 ──
{ type:'kp', icon:'🏆', title:'应用题——方案选择与比赛积分',
content:`<p><strong>方案选择</strong>:分别算总价再比较,或者列方程找"临界点"</p>
<p><strong>比赛积分</strong>:胜场得分+平场得分+负场得分=总积分</p>
<p><strong>银行利率</strong>:利息=本金×利率×存期;本息和=本金+利息</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<text x="20" y="22" font-size="8" fill="#e10600">方案:A=30+0.1×200=50B=0.3×200=60→A省</text>
<text x="20" y="38" font-size="8" fill="rgba(255,255,255,0.5)">积分:胜x场、平y场、负z场→3x+y+0z=总分</text>
<text x="20" y="54" font-size="8" fill="#ffd700"><animate attributeName="opacity" values="0.3;1;0.3" dur="2s" repeatCount="indefinite"/>利率:利息=本金×年利率×年数</text>
</svg>
<div class="kj">方案选择:算总额再比大小<br>比赛积分:设胜场数为x,用总场数列式</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'解方程3x-7=5x+3x等于?', opts:['x=5','x=-5','x=2','x=-2'], correct:1, passMsg:'✅ 移项:3x-5x=3+7→-2x=10→x=-5', failMsg:'💡 3x-5x=3+7→-2x=10→x=-5。移项过桥要变号!' },
{ type:'ss', tag:'重点', question:'解方程(x-1)/2-(2x-3)/3=1,去分母后是', opts:['3(x-1)-2(2x-3)=1','3(x-1)-2(2x-3)=6','3x-1-4x-3=6','3(x-1)-2(2x-3)-1=0'], correct:1, passMsg:'✅ 公分母6,每项乘63(x-1)-2(2x-3)=6', failMsg:'💡 公分母=6。注意常数"=1"也要乘6' },
{ type:'ss', tag:'重点', question:'方程2(x-1)-3(x+2)=0的解是', opts:['x=8','x=-8','x=2','x=-2'], correct:1, passMsg:'✅ 2x-2-3x-6=0→-x=8→x=-8', failMsg:'💡 去括号:2x-2-3x-6=0→-x-8=0→x=-8' },
{ type:'ss', tag:'重点', question:'若x=2是方程3x-4=mx的解,则m=', opts:['1','2','-1','0'], correct:0, passMsg:'✅ 代入:6-4=2m→2=2m→m=1', failMsg:'💡 把x=2代入方程:3×2-4=m×2→2=2m→m=1' },
{ type:'ss', tag:'重点', question:'根据等式性质,由x=y可推出', opts:['x+2=y-2','2x=3y','x/2=y/2','x²=y²'], correct:2, passMsg:'✅ 两边同÷2→x/2=y/2', failMsg:'💡 性质2:两边同乘/除同一个不为0的数,等式仍成立' },
{ type:'ss', tag:'重点', question:'解方程2(x-2)-3(4x-1)=9(1-x)x=', opts:['x=-10','x=10','x=-1','x=1'], correct:0, passMsg:'✅ 展开:2x-4-12x+3=9-9x→-10x-1=9-9x→-x=10→x=-10', failMsg:'💡 去括号仔细:-3(4x-1)=-12x+39(1-x)=9-9x' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'车间28人生产螺栓螺母,每人每天做螺栓12个或螺母18个。1螺栓配2螺母。设x人做螺栓,方程正确的是', opts:['18(28-x)=2×12x','12x=2×18(28-x)','2×12x=18(28-x)','12x=18(28-x)'], correct:0, passMsg:'✅ 螺母数=2×螺栓数→18(28-x)=2×12x', failMsg:'💡 螺栓=12x,螺母=18(28-x)1配2→螺母=2×螺栓' },
{ type:'ss', tag:'核心', question:'商品进价100元,标价150元,打八折出售,利润为', opts:['10元','20元','30元','50元'], correct:1, passMsg:'✅ 售价=150×0.8=120,利润=120-100=20元', failMsg:'💡 利润=售价-进价。售价=标价×折扣=120120-100=20' },
{ type:'ss', tag:'核心', question:'甲乙相距450km相向而行,甲80km/h,乙70km/h,几小时相遇?', opts:['2小时','3小时','2.5小时','3.5小时'], correct:1, passMsg:'✅ 80t+70t=450→150t=450→t=3', failMsg:'💡 速度和×时间=总路程→150t=450→t=3小时' },
{ type:'ss', tag:'核心', question:'标价180元打九折仍获利20%,进价是', opts:['135元','150元','120元','162元'], correct:0, passMsg:'✅ 售价=162162-x=0.2x→1.2x=162→x=135', failMsg:'💡 售价=180×0.9=162;利润=0.2x162-x=0.2x→x=135' },
{ type:'ss', tag:'核心', question:'甲独做6天,乙独做6天。甲先做2天,剩下的合作还需几天?', opts:['2天','3天','4天','1天'], correct:0, passMsg:'✅ 2/6+x(1/6+1/6)=1→1/3+x/3=1→x=2', failMsg:'💡 甲先完成2/6=1/3;剩余2/3;合效率=1/3;还需2天' },
{ type:'ss', tag:'核心', question:'三个连续奇数和是45,中间数是', opts:['13','15','17','14'], correct:1, passMsg:'✅ (x-2)+x+(x+2)=45→3x=45→x=15', failMsg:'💡 设中间x,三数和=(x-2)+x+(x+2)=45→x=15' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'甲先出发2小时(4km/h),乙再追(6km/h),乙几小时追上?', opts:['4小时','2小时','6小时','3小时'], correct:0, passMsg:'✅ 设t小时:6t=4t+8→2t=8→t=4', failMsg:'💡 甲先走8km。乙追上时:乙路程=甲路程→6t=4t+8→t=4' },
{ type:'ss', tag:'高频', question:'每月通话200分钟,A方案月租30元+0.1元/分,B方案0.3元/分,选哪个?', opts:['A方案','B方案','一样贵','无法确定'], correct:0, passMsg:'✅ A=30+20=50B=60→A省', failMsg:'💡 A=30+200×0.1=50B=200×0.3=60→A省10元' },
{ type:'ss', tag:'高频', question:'解方程(2x+1)/3-(x-1)/6=2x=', opts:['x=3','x=2','x=4','x=1'], correct:0, passMsg:'✅ 乘62(2x+1)-(x-1)=12→4x+2-x+1=12→3x=9→x=3', failMsg:'💡 公分母=6,每项乘6!注意"=2"也要乘6得12' },
{ type:'ss', tag:'高频', question:'一项工程甲独做12天,乙独做18天。合作4天后乙独做还需几天?', opts:['6天','8天','10天','12天'], correct:1, passMsg:'✅ 合作4天做5/9,剩4/9,乙效率1/18,需8天', failMsg:'💡 合作效率=1/12+1/18=5/364天做20/36=5/9;剩4/9→需8天' },
{ type:'ss', tag:'高频', question:'某球队胜3分、平1分、负0分。赛10场得20分,胜5场,平几场?', opts:['5场','4场','3场','6场'], correct:0, passMsg:'✅ 设平x场:5×3+x×1+(10-5-x)×0=20→15+x=20→x=5', failMsg:'💡 胜5场→得15分;设平x场→负10-5-x场;15+x=20→x=5' },
{ type:'ss', tag:'高频', question:'存入银行1000元,年利率2.25%,存2年,本息和是', opts:['1045元','1022.5元','1045元','1050元'], correct:0, passMsg:'✅ 利息=1000×2.25%×2=45,本息和=1045', failMsg:'💡 利息=本金×利率×存期=1000×0.0225×2=45;本息和=1000+45=1045' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'某手机流量套餐:A包月50元含5GB,超出0.1元/MBB包月30元含2GB,超出0.15元/MB。每月用7GB,选哪个方案?', opts:['A方案','B方案','一样','无法确定'], correct:0, passMsg:'✅ 7GB=7168MB。A=50+(7168-5120)×0.1=50+204.8=254.8B=30+(7168-2048)×0.15=30+768=798→A省', failMsg:'💡 先统一单位。A超出2GB×1024×0.1=204.8→总254.8元;B超出5GB=5120→×0.15=768→总798元' },
{ type:'ss', tag:'综合', question:'甲、乙两地相距300km,快车80km/h,慢车60km/h。快车先开30分钟,两车相向而行,慢车开出几小时后相遇?', opts:['2小时','1.5小时','2.5小时','3小时'], correct:0, passMsg:'✅ 快车先走80×0.5=40km。设t小时:80t+60t=300-40→140t=260→t=1.86...→约2小时', failMsg:'💡 快车先走40km,剩余260km。速度和140km/h→260÷140≈1.86小时' },
{ type:'ss', tag:'综合', question:'某校七年级共8个班进行足球赛,胜得3分、平得1分、负得0分。七(1)班比赛全部结束,共得17分,其中负了2场,这个班胜了几场?', opts:['5场','6场','4场','3场'], correct:1, passMsg:'✅ 共赛7场(8队单循环每队7场)。负2场→设胜x场:3x+(7-2-x)=17→3x+5-x=17→2x=12→x=6', failMsg:'💡 8队单循环每队赛7场。胜x场→平7-2-x=5-x场。3x+(5-x)=17→x=6' },
{ type:'ss', tag:'综合', question:'小王将一笔钱存入银行1年,年利率2.5%,到期后本息和5125元,本金是', opts:['5000元','5100元','5025元','5050元'], correct:0, passMsg:'✅ 设本金xx+2.5%x=5125→1.025x=5125→x=5000', failMsg:'💡 x+0.025x=1.025x=5125→x=5000' },
// ── 章节复习 ──
{ type:'review', question:'下列解方程步骤正确的是', opts:['3x+5=0→3x=5','4x-2=x→4x-x=2','2x=6→x=3','x/2=3→x=3/2'], correct:2, passMsg:'✅ 2x=6→x=3✓(两边同÷2', failMsg:'💡 A应为3x=-5B应为4x-x=2→3x=2→x=2/3C✓;D应为x=6' },
{ type:'review', question:'甲乙两地240km,一轮船往返需10小时(顺流5小时,逆流5小时),求船速和水速', opts:['船24km/h 水0','船26km/h 水2','船25km/h 水1','船28km/h 水4'], correct:0, passMsg:'✅ 顺=240÷5=48,逆=240÷5=48→船速48,水速0(静水)', failMsg:'💡 顺流=船+水=48,逆流=船-水=48→船速=48,水速=0(静水行船)' },
{ type:'review', question:'某电信公司话费:A方案无月租0.4元/分,B方案月租18元+0.2元/分。每月通话多少分钟时两方案费用相同?', opts:['60分','90分','120分','150分'], correct:1, passMsg:'✅ 设x分钟:0.4x=18+0.2x→0.2x=18→x=90', failMsg:'💡 A=0.4xB=18+0.2x。相等→0.4x=18+0.2x→x=90分钟' },
{ type:'review', question:'某商品标价200元,打八折后仍获利60元,进价是', opts:['120元','100元','80元','140元'], correct:1, passMsg:'✅ 售价=160,进价x160-x=60→x=100', failMsg:'💡 售价=200×0.8=160;利润=售价-进价=60→进价=100' }
]},
// ═══════════════════════════════════════
// CHAPTER 4: 几何图形初步
// ═══════════════════════════════════════
{ id:4, title:'几何图形初步', desc:'立体·平面·线段·射线·角', emoji:'📐',
segs:[
// ── 概述 ──
{ type:'pn', content:`<h3>📐 几何 = 认识空间与形状</h3><p>几何图形分<strong>立体图形</strong>3D·能拿在手里)和<strong>平面图形</strong>2D·画在纸上)</p><p>本章路线:立体图形→线段/射线/直线→角→余角补角→方位角→钟面角</p><svg viewBox="0 0 360 45" style="width:100%;margin:4px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:6px 0;"><line x1="18" y1="22" x2="342" y2="22" stroke="rgba(255,255,255,0.06)" stroke-width="1"/><circle cx="40" cy="22" r="5" fill="#e10600"/><text x="40" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">立体</text><circle cx="105" cy="22" r="5" fill="#e10600"/><text x="105" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">线段/射线</text><circle cx="180" cy="22" r="5" fill="#e10600"/><text x="180" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">角</text><circle cx="250" cy="22" r="5" fill="#e10600"/><text x="250" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">余角/补角</text><circle cx="320" cy="22" r="5" fill="#e10600"/><text x="320" y="39" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">方位/钟面</text></svg>` },
// ── KP1: 立体图形与展开图 ──
{ type:'kp', icon:'📦', title:'立体图形与展开图',
content:`<p><strong>立体图形</strong>:正方体、长方体、圆柱、圆锥、球、棱柱、棱锥</p>
<p><strong>三视图</strong>:从正面看(主视图)、从左面看(左视图)、从上面看(俯视图)</p>
<svg viewBox="0 0 360 100" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px;">
<g>
<animateTransform attributeName="transform" type="rotate" from="0 90 48" to="360 90 48" dur="8s" repeatCount="indefinite"/>
<polygon points="70,30 90,18 115,30 95,42" fill="rgba(225,6,0,0.08)" stroke="rgba(225,6,0,0.2)" stroke-width="1"/>
<polygon points="70,30 90,18 90,48 70,60" fill="rgba(225,6,0,0.04)" stroke="rgba(225,6,0,0.1)" stroke-width="0.5"/>
<polygon points="90,18 115,30 115,60 90,48" fill="rgba(225,6,0,0.06)" stroke="rgba(225,6,0,0.15)" stroke-width="0.5"/>
</g>
<text x="90" y="78" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">旋转正方体</text>
<text x="220" y="18" font-size="7" fill="#ffd700" text-anchor="middle" font-weight="700">正方体展开图11种</text>
<rect x="180" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="196" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="212" y="24" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="180" y="40" width="16" height="16" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<rect x="196" y="40" width="16" height="16" fill="rgba(255,215,0,0.04)" stroke="rgba(255,215,0,0.2)" stroke-width="0.8"/>
<rect x="212" y="40" width="14" height="14" fill="none" stroke="rgba(255,215,0,0.1)" stroke-width="0.8"/>
<text x="180" y="60" font-size="6" fill="rgba(255,255,255,0.15)" text-anchor="middle">"一四一"型</text>
<!-- Animated unfold: squares sliding into net position -->
<rect x="295" y="24" width="14" height="14" fill="none" stroke="rgba(46,204,113,0.2)" stroke-width="0.8">
<animate attributeName="x" values="295;180" dur="3s" repeatCount="indefinite"/>
<animate attributeName="y" values="24;40" dur="3s" repeatCount="indefinite"/>
</rect>
<rect x="310" y="24" width="14" height="14" fill="none" stroke="rgba(46,204,113,0.2)" stroke-width="0.8">
<animate attributeName="x" values="310;196" dur="3s" repeatCount="indefinite"/>
<animate attributeName="y" values="24;56" dur="3s" repeatCount="indefinite"/>
</rect>
<text x="220" y="82" font-size="7" fill="rgba(46,204,113,0.3)" text-anchor="middle">
展开图演示:方块从正方体滑出
<animate attributeName="opacity" values="0;1;1;0" dur="3s" repeatCount="indefinite"/>
</text>
</svg>
<div class="kj">立体图形分两类:多面体全是平,旋转体有曲面<br>正方体展开图:一四一、二三一、二二二、三三</div>
<div class="qj">展开图口诀:"一四一"有6种,"二三一"有3种,"二二二"1种,"三三"1种<br>对面不相邻——展开图中隔一个正方形的是对面</div>` },
// ── KP2: 线段、射线、直线 ──
{ type:'kp', icon:'🔦', title:'线段、射线、直线',
content:`<p>🔸 <strong>线段</strong>:两个端点,有长度——像一段绳子(AB或BA)</p>
<p>🔸 <strong>射线</strong>:一个端点,向一方无限延伸——像手电筒光(从A出发记作AB,A是端点)</p>
<p>🔸 <strong>直线</strong>:无端点,向两方无限延伸——像无限公路(AB或BA)</p>
<svg viewBox="0 0 360 80" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<text x="20" y="18" font-size="8" fill="#e10600" font-weight="700">线段</text>
<line x1="20" y1="28" x2="100" y2="28" stroke="#e10600" stroke-width="2"/>
<circle cx="20" cy="28" r="3" fill="#e10600"/><circle cx="100" cy="28" r="3" fill="#e10600"/>
<text x="60" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">两个端点A、B</text>
<!-- Evolution arrow -->
<text x="108" y="32" font-size="9" fill="rgba(255,255,255,0.1)">→</text>
<text x="120" y="18" font-size="8" fill="#ffd700" font-weight="700">射线</text>
<circle cx="120" cy="28" r="3" fill="#ffd700"/>
<line x1="120" y1="28" x2="180" y2="28" stroke="#ffd700" stroke-width="2">
<animate attributeName="x2" values="150;220;150" dur="2.5s" repeatCount="indefinite"/>
</line>
<polygon points="218,23 228,28 218,33" fill="#ffd700">
<animate attributeName="opacity" values="1;0.3;1" dur="2.5s" repeatCount="indefinite"/>
</polygon>
<text x="160" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">一个端点,无限延伸</text>
<text x="230" y="18" font-size="8" fill="#2ecc71" font-weight="700">直线</text>
<line x1="230" y1="28" x2="340" y2="28" stroke="#2ecc71" stroke-width="2"/>
<polygon points="338,23 348,28 338,33" fill="#2ecc71"/>
<polygon points="232,23 222,28 232,33" fill="#2ecc71"/>
<text x="280" y="42" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">两端无限延伸</text>
<text x="20" y="62" font-size="8" fill="rgba(255,255,255,0.4)">📌 两点确定一条直线 &nbsp; 两点之间线段最短</text>
</svg>
<div class="kj">线段两个点,射线一个点,直线没有点<br>射线看端点,直线看方向,线段看长短</div>
<div class="qj">线段计算设k法:遇到比例直接设每份为k,列方程求解!</div>` },
// ── KP3: 角与余角补角 ──
{ type:'kp', icon:'📐', title:'角与余角补角',
content:`<p><strong>角</strong>:从一点出发的两条射线组成的图形——公共端点是<strong>顶点</strong></p>
<p>⏱ 度分秒是<strong>60进制</strong>1°=60′(分),1=60″(秒)</p>
<svg viewBox="0 0 360 80" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<line x1="30" y1="65" x2="150" y2="65" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"/>
<line x1="30" y1="65" stroke="#ffd700" stroke-width="2">
<animateTransform attributeName="transform" type="rotate" values="0 30 65;55 30 65;0 30 65" dur="3s" repeatCount="indefinite"/>
</line>
<!-- Animated arc showing angle -->
<path d="M 30,65 L 45,65" stroke="#ffd700" stroke-width="1.5" fill="none" opacity="0.3">
<animateTransform attributeName="transform" type="rotate" values="0 30 65;55 30 65;0 30 65" dur="3s" repeatCount="indefinite"/>
</path>
<text x="50" y="48" font-size="8" fill="#ffd700">∠=55°</text>
<text x="110" y="20" font-size="8" fill="#ffd700">∠AOB</text>
<text x="15" y="68" font-size="8" fill="#ffd700">O</text>
<text x="152" y="68" font-size="7" fill="rgba(255,255,255,0.2)">A</text>
<text x="98" y="12" font-size="7" fill="rgba(255,255,255,0.2)">B</text>
<text x="200" y="22" font-size="9" fill="#2ecc71" font-weight="700">余角 + 补角</text>
<text x="200" y="40" font-size="8" fill="rgba(255,255,255,0.5)">余角:∠A+∠B=90°</text>
<text x="200" y="55" font-size="8" fill="rgba(255,255,255,0.5)">补角:∠A+∠B=180°</text>
<text x="200" y="70" font-size="7" fill="#ffd700">x°的余角=90°−x° 补角=180°−x°</text>
</svg>
<div class="kj">度分秒是60进制,借1度换60分<br>余角和为90度,补角和为180度</div>
<div class="qj">角度计算列方程:看到"倍""比""多""少"就设x<br>例:补角是余角的3倍→180-x=3(90-x)→x=45°</div>` },
// ── KP4: 方位角与钟面角 ──
{ type:'kp', icon:'🧭', title:'方位角与钟面角',
content:`<p>🧭 <strong>方位角</strong>:从正北(或正南)方向旋转到目标方向的角度</p>
<p>⏰ <strong>钟面角</strong>:钟面上时针和分针的夹角</p>
<svg viewBox="0 0 360 85" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:8px 0;">
<circle cx="80" cy="42" r="30" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<text x="80" y="10" font-size="7" fill="#e10600" text-anchor="middle" font-weight="700">北</text>
<text x="80" y="80" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="middle">南</text>
<text x="114" y="45" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="start">东</text>
<text x="46" y="45" font-size="7" fill="rgba(255,255,255,0.15)" text-anchor="end">西</text>
<line x1="80" y1="42" x2="80" y2="16" stroke="rgba(255,255,255,0.06)" stroke-width="1"/>
<line x1="80" y1="42" stroke="#ffd700" stroke-width="2">
<animateTransform attributeName="transform" type="rotate" values="0 80 42;50 80 42;0 80 42" dur="3s" repeatCount="indefinite"/>
</line>
<text x="108" y="16" font-size="6" fill="#ffd700">北偏东50°</text>
<circle cx="250" cy="42" r="28" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
<!-- 8:30 specific: hour hand at 255° (8.5h×30°), minute hand at 180° (30min×6°) -->
<line x1="250" y1="42" x2="250" y2="18" stroke="#ffd700" stroke-width="2.5">
<animate attributeName="x2" values="250;257;250" dur="0.5s" repeatCount="indefinite"/>
<animate attributeName="y2" values="18;14;18" dur="0.5s" repeatCount="indefinite"/>
</line>
<line x1="250" y1="42" x2="260" y2="30" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"/>
<circle cx="250" cy="42" r="3" fill="#e10600"/>
<!-- Angle arc showing 75° -->
<path d="M 250,14 A 28,28 0 0,1 261,30" stroke="#ffd700" stroke-width="1.5" fill="none" opacity="0.4"/>
<text x="262" y="16" font-size="7" fill="#ffd700">8:30</text>
<text x="262" y="26" font-size="6" fill="rgba(255,255,255,0.3)">75°</text>
<circle cx="250" cy="42" r="3" fill="#e10600"/>
<text x="250" y="78" font-size="7" fill="rgba(255,255,255,0.2)" text-anchor="middle">8:30 时针=255° 分针=180°</text>
</svg>
<div class="kj">方位角互看:南北交换、东西交换、角度不变<br>A在北偏东50°→O在A的南偏西50°</div>
<div class="qj">钟面角公式:|30h-5.5m|h小时,m分钟)<br>时针每分钟转0.5°,分针每分钟转6°<br>8:30代入:|30×8-5.5×30|=|240-165|=75°</div>` },
// ── KP5: 几何解题模型(交互拖拽)──
{ type:'kp', icon:'🎯', title:'几何解题模型',
content:`<p><strong>模型一:线段双中点模型</strong>——M是AC中点,N是BC中点 → MN = AB/2</p>
<div class="model-wrap" data-model="midpoint">${GeoModel.renderMidpoint()}</div>
<p><strong>模型二:角平分线模型</strong>——OC平分∠AOB → ∠AOC=∠COB=∠AOB/2</p>
<div class="model-wrap" data-model="angle">${GeoModel.renderAngleBisector()}</div>
<p><strong>模型三:分类讨论模型</strong>——看到"直线AB"立刻想:在线段上?在外侧?</p>
<svg viewBox="0 0 360 55" style="width:100%;margin:6px 0;background:rgba(0,0,0,0.12);border-radius:4px;padding:4px 0;">
<text x="15" y="16" font-size="7" fill="#e10600">①C在线段AB上→AC=5</text>
<line x1="15" y1="24" x2="160" y2="24" stroke="rgba(255,215,0,0.15)" stroke-width="2"/>
<circle cx="15" cy="24" r="3" fill="#ffd700"/><text x="15" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">A</text>
<circle cx="80" cy="24" r="3" fill="#e10600"/><text x="80" y="38" font-size="6" fill="#e10600" text-anchor="middle">C</text>
<circle cx="160" cy="24" r="3" fill="#ffd700"/><text x="160" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">B</text>
<text x="190" y="16" font-size="7" fill="#2ecc71">②C在AB外→AC=11</text>
<line x1="190" y1="24" x2="340" y2="24" stroke="rgba(255,215,0,0.15)" stroke-width="2"/>
<circle cx="190" cy="24" r="3" fill="#ffd700"/><text x="190" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">A</text>
<circle cx="270" cy="24" r="3" fill="#ffd700"/><text x="270" y="38" font-size="6" fill="rgba(255,255,255,0.2)" text-anchor="middle">B</text>
<circle cx="330" cy="24" r="3" fill="#e10600"/><text x="330" y="38" font-size="6" fill="#e10600" text-anchor="middle">C</text>
<line x1="270" y1="24" x2="340" y2="24" stroke="#e10600" stroke-width="2" class="animPulse"/>
</svg>
<div class="qj">💡 掌握三个模型,秒杀几何题:<br>① 双中点→MN=AB/2(不管C在哪)<br>② 角平分线→看到平分就÷2<br>③ 分类讨论→看到"直线"马上想两种情况</div>` },
// ── 分站赛入口 ──
{ type:'stg', title:'\u{1F3C1} 分站赛' },
// ── 重点题 ──
{ type:'ss', tag:'重点', question:'下面哪个是棱柱?', opts:['球体','圆锥','三棱柱','圆柱'], correct:2, passMsg:'✅ 三棱柱所有面都是平面', failMsg:'💡 棱柱=所有面都是平面,两头一样。三棱柱✓球✗圆锥✗圆柱✗' },
{ type:'ss', tag:'重点', question:'从正面看一个圆锥,看到的平面图形是', opts:['圆形','三角形','正方形','梯形'], correct:1, passMsg:'✅ 圆锥的主视图=等腰三角形', failMsg:'💡 圆锥正面看是等腰三角形(底面看成立线),上面看是圆' },
{ type:'ss', tag:'重点', question:'手电筒射出的光线在数学上可以看成', opts:['线段','射线','直线','曲线'], correct:1, passMsg:'✅ 手电筒光从灯泡射出→射线', failMsg:'💡 射线:一个端点,向一方无限延伸。手电筒光只有一个起点→射线' },
{ type:'ss', tag:'重点', question:'连接A、B两点的所有连线中,最短的是', opts:['直线','线段','射线','折线'], correct:1, passMsg:'✅ 两点之间线段最短', failMsg:'💡 基本事实:两点之间<strong>线段最短</strong>。注意是"线段"不是"直线"' },
{ type:'ss', tag:'重点', question:'下列说法正确的是?', opts:['射线AB和射线BA是同一射线','延长直线AB','两点确定一条直线','线段AB和线段BA不同'], correct:2, passMsg:'✅ "两点确定一条直线"是几何基本事实', failMsg:'💡 射线AB从A出发,BA从B出发→不同方向;直线无限长不能"延长";线段AB=BA' },
{ type:'ss', tag:'重点', question:'一个棱柱有10个顶点,则它的棱数是', opts:['12','10','20','15'], correct:3, passMsg:'✅ 2n=10→n=5(五棱柱),棱数=3n=15', failMsg:'💡 n棱柱:顶点2n,棱3n,面n+2。2n=10→n=5→棱数=3×5=15' },
// ── 核心题 ──
{ type:'ss', tag:'核心', question:'∠A=42°18′,∠A的余角是多少?', opts:['47°42','137°42','48°42','47°82'], correct:0, passMsg:'✅ 90°=89°60′,89°60′−42°18=47°42', failMsg:'💡 90°写成89°60′(借1°换60′)。度分秒是<strong>60进制</strong>' },
{ type:'ss', tag:'核心', question:'一个角的补角是它余角的3倍,这个角是', opts:['30°','45°','60°','90°'], correct:1, passMsg:'✅ 设x180-x=3(90-x)→x=45°', failMsg:'💡 设角x180-x=3(90-x)→180-x=270-3x→2x=90→x=<strong>45°</strong>' },
{ type:'ss', tag:'核心', question:'8:30时,钟面上时针和分针的夹角是', opts:['60°','75°','80°','90°'], correct:1, passMsg:'✅ (8.5-6)×30°=75°', failMsg:'💡 8:30分针在6,时针在8.5。差2.5格×30°=<strong>75°</strong><br>公式:|30×8-5.5×30|=|240-165|=75°' },
{ type:'ss', tag:'核心', question:'一个角比它的补角小40°,这个角是', opts:['110°','70°','55°','35°'], correct:1, passMsg:'✅ x=(180-x)-40→x=70°', failMsg:'💡 x=180-x-40→2x=140→x=<strong>70°</strong>。验算:70+110=180✓' },
{ type:'ss', tag:'核心', question:'点C在直线AB上,AB=8BC=3,则AC等于', opts:['8 或 3','5','11','5 或 11'], correct:3, passMsg:'✅ C在AB间→AC=5C在AB延长线上→AC=11', failMsg:'💡 没说"在线段上"!分类讨论:①C在AB间→5;②C在B右侧→11' },
{ type:'ss', tag:'核心', question:'∠AOB=80°,OC平分∠AOBOD平分∠BOC,则∠AOD=', opts:['80°','40°','60°','20°'], correct:2, passMsg:'✅ ∠AOC=40°,∠COD=20°,∠AOD=60°', failMsg:'💡 OC平分80°→∠AOC=40°,OD平分40°→∠COD=20°→∠AOD=60°' },
// ── 高频题 ──
{ type:'ss', tag:'高频', question:'AM:MB=3:2AN:NB=5:1M、N在线段AB上,MN=7,则AB=', opts:['AB=25','AB=28','AB=30','AB=35'], correct:2, passMsg:'✅ 双设k→AB=30', failMsg:'💡 AM:MB=3:2→AB=5kAN:NB=5:1→AB=6t→5k=6t→t=5k/6<br>MN=|3k-5t|=7k/6=7→k=6→AB=30' },
{ type:'ss', tag:'高频', question:'2:15到2:45,时针转过的角度是', opts:['180°','30°','15°','90°'], correct:2, passMsg:'✅ 30分钟×0.5°/分=15°', failMsg:'💡 时针每分钟转0.5°,30分钟×0.5°=<strong>15°</strong>。分针转180°但时针只转15°' },
{ type:'ss', tag:'高频', question:'点A在点O的北偏东50°方向,则点O在点A的', opts:['南偏西40°','南偏东50°','北偏西40°','南偏西50°'], correct:3, passMsg:'✅ 南北交换、东西交换、角度不变→南偏西50°', failMsg:'💡 北→南,东→西,50°不变→<strong>南偏西50°</strong>' },
{ type:'ss', tag:'高频', question:'C在线段AB上,AC:CB=2:3M是AC中点,N是CB中点。MN=5,求AB', opts:['AB=8','AB=10','AB=12','AB=15'], correct:1, passMsg:'✅ 设k→AB=10', failMsg:'💡 AC=2k,CB=3k→AB=5k。MC=k,CN=1.5k→MN=2.5k=5→k=2→AB=10' },
{ type:'ss', tag:'高频', question:'正方体的展开图不可能是', opts:['一四一型','二三一型','三三型','五连方'], correct:3, passMsg:'✅ 没有5个连在一起的展开图', failMsg:'💡 正方体展开图共11种:一四一(6)、二三一(3)、二二二(1)、三三(1)。五连方不行!' },
{ type:'ss', tag:'高频', question:'下列哪个是正方体展开图?', opts:['田字形','L形','一四一型','T字形'], correct:2, passMsg:'✅ "一四一"是正方体展开图', failMsg:'💡 展开图口诀:一四一、二三一、二二二、三三。田字、L形、T字都不是' },
// ── 综合题 ──
{ type:'ss', tag:'综合', question:'从O点引三条射线OA、OB、OC,∠AOB=80°,∠BOC=50°,则∠AOC=', opts:['30°或130°','130°','30°','50°'], correct:0, passMsg:'✅ OC在内→30°;OC在外→130°', failMsg:'💡 分类:①OC在∠AOB内→80-50=30°;②OC在外→80+50=130°' },
{ type:'ss', tag:'综合', question:'钟面上3点多少分时时针分针第一次重合?', opts:['16分','16分21秒','16分11秒','15分'], correct:2, passMsg:'✅ 90=5.5x→x≈16.36分=16分22秒', failMsg:'💡 |30×3-5.5x|=0→90-5.5x=0→x≈16.36分=16分22秒' },
{ type:'ss', tag:'综合', question:'一个多面体V=6F=5,欧拉公式求棱数E', opts:['7','8','9','10'], correct:2, passMsg:'✅ V+F-E=2→6+5-E=2→E=9', failMsg:'💡 欧拉公式:<strong>V+F-E=2</strong>→6+5-E=2→E=<strong>9</strong>' },
{ type:'ss', tag:'综合', question:'∠A与∠B互余,∠B与∠C互补,∠A=30°,则∠C=', opts:['60°','120°','150°','90°'], correct:1, passMsg:'✅ ∠A+∠B=90°→∠B=60°;∠B+∠C=180°→∠C=120°', failMsg:'💡 ∠B=90-30=60°,∠C=180-60=<strong>120°</strong>' },
// ── 章节复习 ──
{ type:'review', question:'下面说法正确的是', opts:['射线AB和射线BA是同一条','两点之间直线最短','一个角的补角一定大于这个角','同角的余角相等'], correct:3, passMsg:'✅ 同角的余角相等', failMsg:'💡 A✗方向不同;B✗两点之间<strong>线段</strong>最短;C✗钝角补角更小' },
{ type:'review', question:'若∠A=60°,则∠A的补角比余角大', opts:['30°','60°','90°','120°'], correct:2, passMsg:'✅ 补角=120°,余角=30°,差=90°', failMsg:'💡 补角=180-60=120°,余角=90-60=30°,120-30=<strong>90°</strong>' },
{ type:'review', question:'线段AB=10AC=4M是AC中点,N是BC中点,则MN=', opts:['3','5','7','2'], correct:1, passMsg:'✅ MC=2BC=6CN=3MN=5', failMsg:'💡 AC=4→MC=2BC=6→CN=3MN=2+3=<strong>5</strong>' },
{ type:'review', question:'在直线l上取A、B、C三点,AB=5BC=3,则AC=', opts:['2或8','8','2','无法确定'], correct:0, passMsg:'✅ C在AB间→2C在AB外→8', failMsg:'💡 分类:①C在A、B之间→5-3=2;②C在B右侧→5+3=8' },
{ type:'review', question:'钟面上9:30时针与分针的夹角', opts:['90°','105°','75°','120°'], correct:1, passMsg:'✅ |30×9-5.5×30|=|270-165|=105°', failMsg:'💡 公式:\|30h-5.5m\|=\|270-165\|=<strong>105°</strong>' }
]},
];
// SVG path for car positions (4 checkpoints)
const CP_POS = [
{ x:280, y:68 },
{ x:80, y:200 },
{ x:280, y:340 },
{ x:120, y:468 }
];
const SVG_PATH = 'M 180,20 C 320,20 320,120 180,140 C 40,160 40,260 180,280 C 320,300 320,400 180,420 C 40,440 40,520 180,520';
// ===== 状态 =====
let S = { pts:0, stars:[0,0,0,0], curSS:0, curSeg:0, wrongQ:[], doneQ:[], seenQ:[], viewMode:'normal', wrIdx:0 };
function ld() { try { const d=localStorage.getItem('rb2'); if(d){const p=JSON.parse(d);Object.assign(S,p);} }catch(e){} }
function sv() { try { localStorage.setItem('rb2',JSON.stringify(S)); }catch(e){} }
ld();
// ===== DOM =====
const $ = id => document.getElementById(id);
const qs = (s,el) => (el||document).querySelector(s);
const page = id => { document.querySelectorAll('.pg').forEach(p=>p.classList.remove('act')); $(id).classList.add('act'); };
// ===== 导航 =====
function goMap() { page('pH'); sync(); renderMap(); }
function goChap(idx) {
if (idx<0 || idx>3) return;
if (idx>0 && S.stars[idx-1]===0) return;
S.curSS=idx; S.curSeg=0;
page('pC'); renderSeg(); sync();
}
// ===== 答题地图 =====
function qNum(segIdx) {
let n = 0;
const s = SS[S.curSS];
for (let i = 0; i <= segIdx; i++) { if (s.segs[i].type === 'ss') n++; }
return n;
}
function showAnswerMap() {
const s = SS[S.curSS];
page('pM');
$('mGl').textContent = '\u{1F4CA} \u7B54\u9898\u5730\u56FE';
$('mTi').textContent = s.emoji + ' ' + s.title;
$('mDe').textContent = '\u70B9\u51FB\u9898\u76EE\u8DF3\u8F6C \u00B7 \u7EFF=\u6B63\u786E \u7EA2=\u9519\u8BEF \u7070=\u672A\u7B54';
let html = '<div class="qGrid">';
s.segs.forEach(function(seg, i) {
if (seg.type !== 'ss' && seg.type !== 'review' && seg.type !== 'rd') return;
const cls = seg._answered ? (seg._passed ? 'correct' : 'wrong') : 'unanswered';
const icon = seg._answered ? (seg._passed ? '\u2705' : '\u274C') : '\u25CB';
html += '<div class="qCell ' + cls + '" data-si="' + i + '">';
html += '<div class="qNum">' + qNum(i) + '</div>';
html += '<div class="qSt">' + icon + '</div></div>';
});
html += '</div>';
$('mGrid').innerHTML = html;
}
function goToSeg(idx) {
S.curSeg = idx;
page('pC'); renderSeg(); sync();
}
function firstUnanswered() {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
const t = s.segs[i].type;
if ((t === 'ss' || t === 'review' || t === 'rd') && !s.segs[i]._answered) { goToSeg(i); return; }
}
for (let i = 0; i < s.segs.length; i++) {
const t = s.segs[i].type;
if (t === 'ss' || t === 'review' || t === 'rd') { goToSeg(i); return; }
}
}
function firstWrong() {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
const seg = s.segs[i]; const t = seg.type;
if ((t === 'ss' || t === 'review' || t === 'rd') && seg._answered && !seg._passed) { goToSeg(i); return; }
}
firstUnanswered();
}
function resetAllSegs() {
const s = SS[S.curSS];
s.segs.forEach(function(seg) {
if (seg.type === 'ss' || seg.type === 'review' || seg.type === 'rd') { delete seg._answered; delete seg._selected; delete seg._passed; }
});
showAnswerMap();
}
// ===== 事件委托 =====
function closest(el, sel) {
var e = el;
while (e && e !== document) {
if (e.matches && e.matches(sel)) return e;
e = e.parentElement || e.parentNode;
}
return null;
}
document.addEventListener('click', e => {
const cp = closest(e.target, '.cp');
if (cp) {
const idx = parseInt(cp.getAttribute('data-idx'));
if (!isNaN(idx) && !cp.classList.contains('locked')) { goChap(idx); return; }
}
if (closest(e.target, '#bkMap')) { goMap(); return; }
if (closest(e.target, '#bkMapBtn')) { showAnswerMap(); return; }
if (closest(e.target, '#bkMapM')) { goMap(); return; }
if (closest(e.target, '#bkChapM')) { S.viewMode='normal'; page('pC'); renderSeg(); sync(); return; }
const qc = closest(e.target, '.qCell');
if (qc) { const si = parseInt(qc.getAttribute('data-si')); if (!isNaN(si)) { goToSeg(si); return; } }
if (closest(e.target, '#mUn')) { firstUnanswered(); return; }
if (closest(e.target, '#stgContinue')) { nextSeg(); return; }
if (closest(e.target, '#mErr')) { firstWrong(); return; }
if (closest(e.target, '#mAll')) { if (confirm('\u{1F504} \u786E\u5B9A\u91CD\u7F6E\u672C\u7AE0\u6240\u6709\u7B54\u9898\u72B6\u6001\uFF1F')) { resetAllSegs(); } return; }
const optEl = closest(e.target, '.opt');
if (optEl && !optEl.classList.contains('disabled')) {
const idx = parseInt(optEl.getAttribute('data-idx'));
if (!isNaN(idx)) { pickAns(idx); return; }
}
if (closest(e.target, '#egBtn')) { stepEg(); return; }
const actEl = closest(e.target, '.act');
if (actEl && actEl.classList.contains('show')) { nextSeg(); return; }
if (closest(e.target, '#pmNt')) { closePodium(); return; }
if (closest(e.target, '#mBtn')) { toggleBGM(); return; }
const wrRm = closest(e.target, '.wrRm'); if (wrRm) {
const id = wrRm.getAttribute('data-id'); const wi = S.wrongQ.indexOf(id);
if (wi>=0) { S.wrongQ.splice(wi,1); if (S.wrIdx>=S.wrongQ.length) S.wrIdx=0; }
sv(); renderWrongList();
return;
}
const wrDo = closest(e.target, '.wrDo'); if (wrDo) {
const id = wrDo.getAttribute('data-id'); const [ci,si] = id.split('_').map(Number);
S.curSS=ci; S.curSeg=si; S.viewMode='normal';
const seg=SS[ci].segs[si];
if (seg) { delete seg._answered; delete seg._selected; delete seg._passed; }
page('pC'); renderSeg(); sync();
return;
}
if (closest(e.target, '#wrongBtn')) { goWrongReview(); return; }
if (closest(e.target, '#dbBtn')) {
const p = $('dbPanel');
if (p.style.display === 'none' || !p.style.display) { renderDashboard(); p.style.display = 'block'; }
else { p.style.display = 'none'; }
return;
}
if (closest(e.target, '#stgStart')) {
const s = SS[S.curSS];
for (let i = 0; i < s.segs.length; i++) {
if (s.segs[i].tag && !s.segs[i]._answered) { goToSeg(i); return; }
}
// all done
for (let i = 0; i < s.segs.length; i++) { if (s.segs[i].tag) { goToSeg(i); return; } }
return;
}
if (closest(e.target, '#stgDone')) { renderStgResult(); return; }
if (closest(e.target, '#bkMapW')) { S.viewMode='normal'; goMap(); return; }
if (closest(e.target, '#clrP')) {
if (confirm('\u{1F6D1} 确定清除所有进度?本地记录将被清空。')) {
localStorage.removeItem('rb2');
Object.assign(S,{pts:0,stars:[0,0,0,0],curSS:0,curSeg:0,wrongQ:[],doneQ:[],seenQ:[],viewMode:'normal',wrIdx:0});
sv(); goMap();
}
return;
}
});
// ===== 交互式几何模型拖拽系统 =====
function initDragModels(){
document.querySelectorAll('.drag-point').forEach(el=>{
el.addEventListener('mousedown',e=>{
e.preventDefault();
const model=el.getAttribute('data-model');
const svg=el.closest('svg');const sR=svg.getBoundingClientRect();
const sx=sR.width/svg.viewBox.animVal.width,sy=sR.height/svg.viewBox.animVal.height;
const minX=parseFloat(el.getAttribute('data-minx')||'0');
const maxX=parseFloat(el.getAttribute('data-maxx')||'360');
let cb;
if(model==='mid'){cb=(x,y)=>{GeoModel.initMidDrag(x,y);};}
else if(model==='angle'){
const bounds=svg.getBoundingClientRect();
cb=(x,y)=>{GeoModel.initAngleDrag(x,y);};
}
else if(model==='clock'){
const hand=el.getAttribute('data-hand');
cb=(x,y)=>{GeoModel.initClockDrag(x,y,hand);};
}
GeoModel.startDrag(e,model,el,{minX,minY:0,maxX:360,maxY:200,cb});
el.setAttribute('data-dragging','true');
});
});
}
document.addEventListener('mousemove',e=>{
if(GeoModel.drag.active)GeoModel.moveDrag(e);
});
document.addEventListener('mouseup',e=>{
if(GeoModel.drag.active){GeoModel.endDrag();}
});
// ===== 轨道地图 =====
function renderMap() {
const svgPath = document.getElementById('trackFill');
const donePath = document.getElementById('trackDone');
const total = 4;
const done = S.stars.filter(s=>s>0).length;
const pathLen = svgPath ? svgPath.getTotalLength() : 800;
// update done path
if (donePath) {
const offset = pathLen - (done / total) * pathLen;
donePath.setAttribute('stroke-dasharray', pathLen);
donePath.setAttribute('stroke-dashoffset', offset);
}
// update checkpoints
document.querySelectorAll('.cp').forEach(el => {
const idx = parseInt(el.getAttribute('data-idx'));
const locked = idx>0 && S.stars[idx-1]===0;
el.classList.toggle('locked', locked);
el.classList.toggle('done', S.stars[idx]>0);
const lockIcon = el.querySelector('.lock-icon');
if (lockIcon) lockIcon.style.display = locked ? 'block' : 'none';
});
// update star text on CPs
for (let i = 0; i < 4; i++) {
const el = $('cps'+i);
if (el) el.textContent = S.stars[i] ? '\u2B50'.repeat(S.stars[i]) : '';
}
// position car
positionCar();
}
function positionCar() {
const car = $('trackCar');
if (!car) return;
const done = S.stars.filter(s=>s>0).length;
let idx = Math.min(done, 3);
const p = CP_POS[idx];
let angle = 0;
if (idx < 3) {
const n = CP_POS[idx + 1];
angle = Math.atan2(n.y - p.y, n.x - p.x) * (180 / Math.PI);
} else if (idx > 0) {
const pv = CP_POS[idx - 1];
angle = Math.atan2(p.y - pv.y, p.x - pv.x) * (180 / Math.PI);
}
car.style.left = ((p.x / 360) * 100) + '%';
car.style.top = ((p.y / 540) * 100) + '%';
car.style.transform = 'translate(-50%,-50%) rotate(' + angle + 'deg)';
}
// ===== 章节 =====
function segLabel(seg, idx) {
if (seg.type==='kp') return seg.title || '知识点';
if (seg.type==='stg') return seg.title || '分站赛';
if (seg.type==='review') return '章节复习';
if (seg.type==='rd') return '计时练习';
if (seg.type==='eg') return '例题讲解';
if (seg.type==='pn') return '路书';
let nth = 0;
SS[S.curSS].segs.slice(0, idx+1).forEach(sg => { if(sg.type==='ss') nth++; });
return ['练习','排位','正赛','冲刺'][nth-1] || '计时';
}
function renderSeg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (!seg) return finishChap();
const co = $('cCo');
co.style.animation = 'none'; void co.offsetHeight;
$('cGl').textContent = `SS ${S.curSS+1} \u00B7 ${segLabel(seg,S.curSeg)}`;
$('cTi').textContent = s.emoji+' '+s.title;
$('cDe').textContent = s.desc;
const mBtn = $('bkMapBtn');
if (mBtn) mBtn.style.display = (seg.type==='ss'||seg.type==='review'||seg.type==='rd'||seg.type==='stg') ? 'inline-block' : 'none';
if (seg.type==='kp') renderKP(seg);
else if (seg.type==='stg') renderStg(seg);
else if (seg.type==='pn') renderPN(seg);
else if (seg.type==='ss'||seg.type==='review') renderSSeg(seg);
else if (seg.type==='rd') renderRd(seg);
else if (seg.type==='eg') renderEg(seg);
co.style.animation = 'fIn 0.35s ease';
}
function renderKP(seg) {
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid #ffd700;"><div class="kpTitle">${seg.icon||'📖'} ${seg.title}</div>${seg.content}<div style="text-align:center;margin-top:14px;"><button class="act show">继续 \u2192</button></div></div>`;
beep('click');
initDragModels();
}
// ===== 分站赛 =====
let stgStat = { total:0, done:0, cor:0, wrg:0 };
function stgReset() {
const s = SS[S.curSS];
stgStat.total = s.segs.filter(function(sg){return sg.tag}).length;
stgStat.done = stgStat.cor = stgStat.wrg = 0;
}
function stgUpdate() {
stgStat.done = 0; stgStat.cor = 0; stgStat.wrg = 0;
const s = SS[S.curSS];
s.segs.forEach(function(sg) {
if (!sg.tag) return;
if (sg._answered) { stgStat.done++; if (sg._passed) stgStat.cor++; else stgStat.wrg++; }
});
}
function renderStg(seg) {
stgReset(); stgUpdate();
const total = stgStat.total;
const done = stgStat.done;
const pct = total ? Math.round(done/total*100) : 0;
const tags = { '重点':0, '核心':0, '高频':0, '综合':0 };
const s = SS[S.curSS];
s.segs.forEach(function(sg) { if (sg.tag) tags[sg.tag] = (tags[sg.tag]||0)+1; });
let tagHtml = '';
Object.keys(tags).forEach(function(t) { if (tags[t]) tagHtml += '<span class="stgTag">'+t+' ×'+tags[t]+'</span>'; });
const bar = '<div class="stgBar"><div class="stgFill" style="width:'+pct+'%"></div></div>';
const status = done ? '<div style="margin:8px 0;font-size:13px;color:rgba(255,255,255,0.5)">已完成 '+done+'/'+total+''+stgStat.cor+' 正确 · '+stgStat.wrg+' 错误)</div>' : '';
const btn = done < total ? '<button class="act show" id="stgStart">'+(done?'继续练习 \u2192':'开始练习 \u2192')+'</button>'
: '<div style="margin:14px 0;padding:14px;background:rgba(46,204,113,0.04);border:1px solid rgba(46,204,113,0.08);border-radius:3px;"><div style="font-size:24px;font-weight:900;color:#2ecc71;">'+stgStat.cor+'/'+total+'</div><div style="font-size:12px;color:rgba(255,255,255,0.3);margin:2px 0 6px;">正确率 '+Math.round(stgStat.cor/total*100)+'%</div><button class="act show" id="stgDone">查看成绩单 \u2192</button></div>';
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid #e10600;"><div class="kpTitle" style="color:#e10600;">\u{1F3C1} 分站赛:${s.title}</div><p>综合练习——检测本章全部知识点掌握情况</p><div style="margin:10px 0;">${tagHtml}</div>${bar}${status}${btn}</div>`;
}
function renderStgResult() {
stgUpdate();
const total = stgStat.total;
const cor = stgStat.cor;
const wrg = stgStat.wrg;
const pct = total ? Math.round(cor/total*100) : 0;
const stars = pct >= 90 ? 3 : pct >= 60 ? 2 : pct >= 30 ? 1 : 0;
// Category breakdown
const s = SS[S.curSS];
const cats = {};
s.segs.forEach(function(sg) {
if (!sg.tag || !sg._answered) return;
if (!cats[sg.tag]) cats[sg.tag] = { total:0, cor:0 };
cats[sg.tag].total++;
if (sg._passed) cats[sg.tag].cor++;
});
let catHtml = '';
Object.keys(cats).forEach(function(t) {
const c = cats[t];
catHtml += '<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid rgba(255,255,255,0.02);"><span style="color:rgba(255,255,255,0.4)">'+t+'</span><span>'+c.cor+'/'+c.total+' <span style="color:'+(c.cor===c.total?'#2ecc71':'rgba(255,255,255,0.2)')+'">('+Math.round(c.cor/c.total*100)+'%)</span></span></div>';
});
const passFail = pct >= 60 ? '<span style="color:#2ecc71">\u2705 通过</span>' : '<span style="color:#e10600">\u274C 未通过,继续加油</span>';
$('cCo').innerHTML = `<div class="pn" style="border-left:3px solid ${pct>=60?'#2ecc71':'#e10600'};text-align:center;">
<div class="kpTitle" style="color:#ffd700;font-size:26px;animation:bounceIn 0.5s ease;">\u{1F3C6} ${stars>=3?'完美!':stars>=2?'不错!':'继续加油!'}</div>
<div style="font-size:40px;font-weight:900;color:#fff;margin:8px 0;font-family:'Inter',monospace;animation:bounceIn 0.6s ease;">${cor}<span style="font-size:20px;color:rgba(255,255,255,0.2);">/${total}</span></div>
<div style="font-size:13px;color:rgba(255,255,255,0.3);margin-bottom:12px;animation:slideUp 0.5s ease 0.2s both;">正确率 ${pct}% ${passFail}</div>
<div style="text-align:left;max-width:240px;margin:0 auto 14px;animation:slideUp 0.5s ease 0.3s both;">${catHtml}</div>
<button class="act show" style="background:${pct>=90?'#ffd700;color:#0a0a1a':pct>=60?'#2ecc71':pct>0?'#e10600':'rgba(255,255,255,0.1)'};animation:bounceIn 0.5s ease 0.5s both;" id="stgContinue">继续 \u2192</button>
</div>`;
}
function renderPN(seg) {
$('cCo').innerHTML = `<div class="pn">${seg.content}<div style="text-align:center;margin-top:12px;"><div style="font-size:10px;color:rgba(255,255,255,0.35);margin-bottom:5px;letter-spacing:2px;font-weight:700;">CO-DRIVER: 路书完毕</div><button class="act show">发车 \u2192</button></div></div>`;
beep('click');
}
function renderSSeg(seg) {
const done = seg._answered||false;
const sel = seg._selected??-1;
const lbl = ['A','B','C','D'];
const qnLabel = seg.type==='review' ? '章节复习' : (seg.tag ? segLabel(seg,S.curSeg) : segLabel(seg,S.curSeg));
const diag = seg.diagram ? `<div class="geoDiag">${seg.diagram}</div>` : '';
let progHtml = '';
if (seg.tag) {
stgUpdate();
const pct = stgStat.total ? Math.round((stgStat.done-1)/stgStat.total*100) : 0;
progHtml = `<div class="stgBar" style="margin:0 0 8px"><div class="stgFill" style="width:${pct}%"></div></div><div style="font-size:10px;color:rgba(255,255,255,0.2);margin-bottom:6px;font-family:'Inter',monospace;"><span style="color:#ffd700">${seg.tag}</span> · ${stgStat.done-1}/${stgStat.total} 答 · ${stgStat.cor}${stgStat.wrg}✗</div>`;
}
const tagBadge = seg.tag ? `<span style="display:inline-block;padding:1px 6px;border:1px solid rgba(255,255,255,0.06);border-radius:2px;font-size:9px;font-weight:700;color:rgba(255,255,255,0.3);margin-left:6px;font-family:'Inter',monospace;">${seg.tag}</span>` : '';
$('cCo').innerHTML = `${progHtml}<div class="qp"><div class="qn">${qnLabel}${tagBadge}</div>${diag}<div class="qt">${seg.question}</div><div class="qo">${seg.opts.map((o,i)=>`<div class="opt ${done?(i===seg.correct?'pass':(i===sel?'fail':'disabled')):''}" data-idx="${i}"><span class="lb">${lbl[i]}</span>${o}</div>`).join('')}</div><div class="qfb ${done?'show '+(sel===seg.correct?'pass':'fail'):''}">${done?(sel===seg.correct?seg.passMsg:seg.failMsg):''}</div>${done?'<button class="act show">继续 \u2192</button>':''}</div>`;
}
function qid() { return S.curSS+'_'+S.curSeg; }
function goWrongReview() {
S.viewMode='wrongReview'; S.wrIdx=0;
page('pW'); renderWrongList();
}
function renderWrongList() {
const $wl = $('wList');
if (!S.wrongQ.length) {
$wl.innerHTML='<div style="text-align:center;padding:40px 20px;color:rgba(255,255,255,0.15);font-size:12px;">\u{1F389} 暂无错题!继续保持!</div>';
return;
}
$wl.innerHTML=S.wrongQ.map((id,i)=>{
const [ci,si]=id.split('_').map(Number);
const seg=SS[ci].segs[si];
if (!seg) return '';
return `<div class="wrItem" data-idx="${i}">
<div class="wrH">SS${ci+1} · ${SS[ci].emoji} ${SS[ci].title} · <span class="wrLb">${segLabel(seg,si)}</span></div>
<div class="wrQ">${seg.question}</div>
<div class="wrAct"><button class="wrDo" data-id="${id}">\u{1F504} 重做</button><button class="wrRm" data-id="${id}">\u{2716} 移除</button></div>
</div>`;
}).join('');
}
function pickAns(idx) {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (seg._answered) return;
seg._answered=true; seg._selected=idx; seg._passed=idx===seg.correct;
const id = qid();
if (!S.doneQ.includes(id)) S.doneQ.push(id);
if (idx===seg.correct) {
S.pts=Math.min(100,S.pts+8); beep('pass'); sparks(6);
const wi = S.wrongQ.indexOf(id);
if (wi>=0) { S.wrongQ.splice(wi,1); if (S.wrIdx>=S.wrongQ.length) S.wrIdx=0; }
} else {
beep('fail');
if (!S.wrongQ.includes(id)) S.wrongQ.push(id);
}
sv(); renderSeg(); sync();
const hb=$('hintBadge'); if(hb)hb.textContent=S.wrongQ.length||'';
}
function renderRd(seg) {
const nums = seg.numbers||[];
const mn = Math.min(...nums)-1, mx = Math.max(...nums)+1, rg = mx-mn||1;
const ticks = []; for(let i=mn;i<=mx;i++) ticks.push(i);
$('cCo').innerHTML = `<div class="rv"><h3>${seg.title||'\u{1F4CD} 赛道定位'}</h3><div style="font-size:9px;color:rgba(255,255,255,0.08);margin-bottom:8px;letter-spacing:1px;">SPLIT TIME</div><div class="rline">${ticks.map(t=>`<span class="mk ${t===0?'z':''}" style="left:${(t-mn)/rg*100}%">${t}</span>`).join('')}${nums.map(n=>`<div class="car" style="left:${(n-mn)/rg*100}%">\u{1F3CE}<span class="vl">${n>0?'+'+n:n}</span></div>`).join('')}</div><button class="act show" style="margin-top:12px;">继续 \u2192</button></div>`;
beep('click');
}
// ===== 例题讲解 (step-through) =====
function renderEg(seg) {
if (!seg._step) seg._step = 1;
const step = seg._step;
const total = seg.steps.length;
$('cCo').innerHTML = `<div class="eg-box">
<div class="eg-title">${seg.title}</div>
<div class="eg-steps">${seg.steps.map((s,i)=>`<div class="eg-step ${i<step?'show':''}"><span class="step-num">${i+1}</span>${s}</div>`).join('')}</div>
${step < total ? `<button class="eg-btn" id="egBtn">下一步 \u2192</button>` : `<button class="act show">继续 \u2192</button>`}
</div>`;
beep('click');
}
function stepEg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
if (!seg || seg.type!=='eg') return;
if (!seg._step) seg._step = 1;
const total = seg.steps.length;
if (seg._step < total) {
seg._step++;
renderEg(seg);
if (seg._step === total) beep('pass');
}
}
function nextSeg() {
const s = SS[S.curSS];
const seg = s.segs[S.curSeg];
const id = S.curSS+'_'+S.curSeg;
if (!S.seenQ.includes(id)) { S.seenQ.push(id); sv(); }
if (S.curSeg < s.segs.length-1) { S.curSeg++; renderSeg(); sync(); }
else finishChap();
}
// ===== 章节结束 → 庆祝 =====
function finishChap() {
const s = SS[S.curSS];
const qs = s.segs.filter(seg=>seg.type==='ss'||seg.type==='review'||seg.type==='rd');
const ok = qs.filter(seg=>seg._passed).length;
const total = qs.length||1;
const r = ok/total;
let stars = 0;
if (r>=0.9) stars=3;
else if (r>=0.6) stars=2;
else if (r>=0.3) stars=1;
if (total===0) stars=3;
S.stars[S.curSS] = Math.max(S.stars[S.curSS], stars);
S.pts = Math.min(100, S.pts + stars*5);
sv();
const pos = ['DNF','P10','P5','P1'];
const cups = ['\u{1F3C1}','\u{1F949}','\u{1F948}','\u{1F3C6}'];
const titles = ['未完赛','P10 \u2014 积分区外','P5 \u2014 得分','SS WIN \u2014 冠军!'];
const msgs = ['再练一圈,下个赛段复仇','继续 push,争取更好成绩','差一点就完美了!','VERSTAPPEN 模式!冲向下一个赛段'];
const nextTxt = S.curSS < 3 && S.stars[S.curSS] > 0 ? '驶向下一站 \u2192' : '返回赛道';
$('pmN').textContent = pos[stars];
$('pmC').textContent = cups[stars];
$('pmTi').textContent = titles[stars];
$('pmSt').textContent = '\u2B50'.repeat(stars)+'\u2B50'.repeat(3-stars);
$('pmXp').textContent = '+'+stars*5+' PTS';
$('pmMs').textContent = msgs[stars];
$('pmNt').textContent = nextTxt;
$('po').classList.add('show');
// big confetti
sparks(50);
beep(stars===3?'win':'pass');
}
function closePodium() {
$('po').classList.remove('show');
const nextIdx = S.curSS + 1;
if (nextIdx < 4 && S.stars[S.curSS] > 0) {
// animate car along track curve
animateCarAlongPath(nextIdx, () => {
updateTrackProgress(nextIdx);
setTimeout(() => { goMap(); }, 300);
});
} else {
goMap();
}
}
const _carPathCache = (function(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', SVG_PATH);
const len = p.getTotalLength();
const samples = 200;
const cpT = CP_POS.map(cp => {
let best = 0, bestD = Infinity;
for (let i = 0; i <= samples; i++) {
const pt = p.getPointAtLength(i/samples*len);
const d = Math.hypot(pt.x-cp.x, pt.y-cp.y);
if (d < bestD) { bestD = d; best = i/samples; }
}
return best;
});
return { p, len, cpT };
})();
function animateCarAlongPath(toIdx, onDone) {
const car = $('trackCar'); if (!car) return void(onDone&&onDone());
const { p, len, cpT } = _carPathCache;
const fromT = S.curSS >= 0 && S.curSS < cpT.length ? cpT[S.curSS] : 0;
const toT = cpT[toIdx];
const duration = 1400;
const start = performance.now();
car.classList.add('moving');
// Hide star/name labels during transit
document.querySelectorAll('.cp-label, .cp-name, .cp-stars').forEach(el => el.style.opacity = '0.3');
function step(time) {
const elapsed = time - start;
const progress = Math.min(1, elapsed / duration);
const ease = 1 - Math.pow(1 - progress, 3);
const t = Math.max(0, Math.min(1, fromT + (toT - fromT) * ease));
const pt = p.getPointAtLength(t * len);
// direction for rotation (look-ahead)
const dt = 0.003;
const pt2 = p.getPointAtLength(Math.max(0, Math.min(len, (t + dt) * len)));
const angle = Math.atan2(pt2.y - pt.y, pt2.x - pt.x) * (180 / Math.PI);
car.style.left = ((pt.x / 360) * 100) + '%';
car.style.top = ((pt.y / 540) * 100) + '%';
car.style.transform = 'translate(-50%,-50%) rotate(' + angle + 'deg)';
if (progress < 1) { requestAnimationFrame(step); }
else {
car.classList.remove('moving');
document.querySelectorAll('.cp-label, .cp-name, .cp-stars').forEach(el => el.style.opacity = '1');
if (onDone) onDone();
}
}
requestAnimationFrame(step);
}
function updateTrackProgress(idx) {
const donePath = document.getElementById('trackDone');
const fillPath = document.getElementById('trackFill');
if (!donePath || !fillPath) return;
const pathLen = fillPath.getTotalLength();
const progress = idx / 4;
donePath.setAttribute('stroke-dasharray', pathLen);
donePath.setAttribute('stroke-dashoffset', pathLen - progress * pathLen);
}
// ===== HUD =====
function sync() {
const done = S.stars.filter(s=>s>0).length;
$('hStage').textContent = Math.min(done+1,4)+'/4';
$('hPts').textContent = S.pts;
}
// ===== 学习总览 =====
function renderDashboard() {
const titles = ['有理数','整式的加减','一元一次方程','几何图形初步'];
const emojis = ['🏁','🔧','🛞','📐'];
let html = '<div id="dbInner" style="background:rgba(8,8,20,0.5);border:1px solid rgba(255,255,255,0.03);border-radius:3px;padding:12px 14px;">';
for (let ch = 0; ch < 4; ch++) {
const s = SS[ch];
const stars = S.stars[ch];
const starStr = stars ? '\u2B50'.repeat(stars) : '\u2606\u2606\u2606';
// Count KP segments seen vs total
let kpTotal = 0, kpSeen = 0;
let ssTotal = 0, ssDone = 0, ssCor = 0;
s.segs.forEach(function(seg, i) {
const id = ch+'_'+i;
if (seg.type === 'kp' || seg.type === 'eg' || seg.type === 'pn') { kpTotal++; if (S.seenQ.includes(id)) kpSeen++; }
if (seg.type === 'ss' || seg.type === 'review' || seg.tag) { ssTotal++; if (seg._answered) { ssDone++; if (seg._passed) ssCor++; } }
});
const kpPct = kpTotal ? Math.round(kpSeen/kpTotal*100) : 0;
const kpBar = '<div class="stgBar" style="margin:2px 0;height:3px;"><div class="stgFill" style="width:'+kpPct+'%;background:'+(kpPct===100?'#2ecc71':'#e10600')+'"></div></div>';
const ssStr = ssDone ? (ssCor+'/'+ssDone) : '0/0';
const status = kpSeen === kpTotal ? (ssDone === ssTotal ? '\u2705 已完成' : '\u{1F3C1} 练习中') : '\u{1F4D6} 学习中';
html += '<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.02);display:flex;align-items:center;gap:8px;">';
html += '<div style="width:24px;font-size:16px;text-align:center;">'+emojis[ch]+'</div>';
html += '<div style="flex:1;min-width:0;">';
html += '<div style="font-size:12px;font-weight:700;color:#fff;">SS'+(ch+1)+' '+titles[ch]+'</div>';
html += '<div style="font-size:9px;color:rgba(255,255,255,0.3);">\u{1F4DA} 学习 '+kpSeen+'/'+kpTotal+' &nbsp;\u270F 答题 '+ssStr+' &nbsp;'+starStr+'</div>';
html += kpBar;
html += '</div>';
html += '<div style="font-size:10px;color:'+(status==='\u2705 已完成'?'#2ecc71':status==='\u{1F3C1} 练习中'?'#ffd700':'rgba(255,255,255,0.3)')+';">'+status+'</div>';
html += '</div>';
}
html += '<div style="font-size:9px;color:rgba(255,255,255,0.15);text-align:center;padding-top:6px;">\u{1F4A1} 点击赛道检查点进入章节 &nbsp;|&nbsp; 绿色=已完成全部学习</div>';
html += '</div>';
$('dbPanel').innerHTML = html;
}
let ax = null;
function ac() { if(!ax) ax=new(window.AudioContext||window.webkitAudioContext)(); return ax; }
function beep(t) {
try {
const ctx = ac();
if (ctx.state === 'suspended') ctx.resume();
if (t==='pass') { [523,659,784].forEach((f,i)=>{const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.03;o.frequency.value=f;o.start(ctx.currentTime+i*0.1);o.stop(ctx.currentTime+i*0.1+0.1);}); }
else if (t==='fail') { const o=ctx.createOscillator(),g=ctx.createGain();o.type='sawtooth';o.connect(g);g.connect(ctx.destination);g.gain.value=0.015;o.frequency.setValueAtTime(200,ctx.currentTime);o.frequency.setValueAtTime(160,ctx.currentTime+0.1);o.start(ctx.currentTime);o.stop(ctx.currentTime+0.2); }
else if (t==='win') { [523,587,659,784,1047].forEach((f,i)=>{const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.03;o.frequency.value=f;o.start(ctx.currentTime+i*0.08);o.stop(ctx.currentTime+i*0.08+0.12);}); }
else { const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);g.gain.value=0.015;o.frequency.value=440;o.start(ctx.currentTime);o.stop(ctx.currentTime+0.04); }
} catch(e) {}
}
// ===== 背景音乐 =====
let bgmNodes = [];
let bgmOn = false;
let bgmTimer = null;
function toggleBGM() {
const ctx = ac();
if (ctx.state === 'suspended') ctx.resume();
if (bgmOn) stopBGM();
else startBGM();
}
function startBGM() {
if (bgmOn) return;
try {
const ctx = ac();
bgmOn = true;
document.getElementById('mBtn').classList.add('on');
const master = ctx.createGain();
master.gain.value = 0;
master.gain.linearRampToValueAtTime(0.25, ctx.currentTime + 1.5);
master.connect(ctx.destination);
bgmNodes.push(master);
const BPM = 130;
const beat = 60 / BPM;
function kick(time) {
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(120, time);
o.frequency.exponentialRampToValueAtTime(40, time + 0.08);
g.gain.setValueAtTime(0.50, time);
g.gain.exponentialRampToValueAtTime(0.001, time + 0.20);
o.connect(g); g.connect(master);
o.start(time); o.stop(time + 0.15);
bgmNodes.push(o, g);
}
function hat(time) {
const n = ctx.createBufferSource();
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.008));
n.buffer = buf;
const g = ctx.createGain();
g.gain.value = 0.10;
n.connect(g); g.connect(master);
n.start(time);
bgmNodes.push(n, g);
}
const bassNotes = [55, 65, 73, 82, 73, 65, 55, 49];
let bassIdx = 0;
function bassLine(time) {
const o = ctx.createOscillator();
o.type = 'sawtooth';
o.frequency.value = bassNotes[bassIdx % bassNotes.length];
const g = ctx.createGain();
g.gain.setValueAtTime(0.12, time);
g.gain.exponentialRampToValueAtTime(0.001, time + beat * 0.95);
const f = ctx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.value = 300;
o.connect(f); f.connect(g); g.connect(master);
o.start(time); o.stop(time + beat * 0.9);
bgmNodes.push(o, g, f);
bassIdx++;
}
const melody = [
[330,0],[349,0.5],[392,1],[440,1.5],
[523,2],[494,2.5],[440,3],[392,3.5],
[349,4],[392,4.5],[440,5],[494,5.5],
[523,6],[587,6.5],[659,7],[587,7.5],
[523,8],[494,8.5],[440,9],[494,9.5],
[523,10],[587,10.5],[659,11],[784,11.5],
[659,12],[587,12.5],[523,13],[494,13.5],
[440,14],[392,14.5],[349,15],[330,15.5],
[349,16],[392,16.5],[440,17],[523,17.5],
[587,18],[523,18.5],[494,19],[440,19.5],
[392,20],[440,20.5],[494,21],[523,21.5],
[587,22],[659,22.5],[784,23],[659,23.5],
[587,24],[523,24.5],[494,25],[440,25.5],
[392,26],[349,26.5],[330,27],[294,27.5],
[261,28],[294,28.5],[330,29],[349,29.5],
[392,30],[440,30.5],[494,31],[523,31.5]
];
function lead(time) {
melody.forEach(([f, off]) => {
const t = time + off * beat;
const o = ctx.createOscillator();
o.type = 'square';
o.frequency.value = f;
const g = ctx.createGain();
const vel = off % 2 < 0.01 ? 0.06 : 0.04;
g.gain.setValueAtTime(vel, t);
g.gain.exponentialRampToValueAtTime(0.001, t + beat * 0.5);
const f2 = ctx.createBiquadFilter();
f2.type = 'lowpass';
f2.frequency.value = 1200;
o.connect(f2); f2.connect(g); g.connect(master);
o.start(t); o.stop(t + beat * 0.6);
bgmNodes.push(o, g, f2);
});
}
function pad(time) {
const chords = [[65,82,98],[73,87,110],[82,98,123],[98,110,130]];
chords.forEach((c, ci) => {
const t = time + ci * 4 * beat;
c.forEach(f => {
const o = ctx.createOscillator();
o.type = 'sine';
o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.04, t + 0.5);
g.gain.setValueAtTime(0.04, t + 3);
g.gain.linearRampToValueAtTime(0.001, t + 4);
const of = ctx.createBiquadFilter();
of.type = 'lowpass';
of.frequency.value = 600;
o.connect(of); of.connect(g); g.connect(master);
o.start(t); o.stop(t + 4 * beat);
bgmNodes.push(o, g, of);
});
});
}
let bar = 0;
function seq() {
if (!bgmOn) return;
const t = ctx.currentTime + 0.05;
const barLen = 4 * beat;
for (let i = 0; i < 4; i++) kick(t + i * beat);
for (let i = 0; i < 4; i++) hat(t + i * beat + beat * 0.5);
for (let i = 0; i < 4; i++) bassLine(t + i * beat);
if (bar % 4 === 0) lead(t + beat);
if (bar % 4 === 0) pad(t);
bar++;
bgmTimer = setTimeout(seq, barLen * 1000);
}
setTimeout(seq, 500);
} catch(e) { bgmOn = false; }
}
function stopBGM() {
bgmOn = false;
document.getElementById('mBtn').classList.remove('on');
clearTimeout(bgmTimer);
bgmNodes.forEach(n => { try { n.disconnect && n.disconnect(); n.stop && n.stop(); } catch(e) {} });
bgmNodes = [];
}
// ===== 粒子 =====
function sparks(n) {
const c = $('pyro');
const cs = ['#e10600','#ffd700','#e10600','#fff','#ffd700','#2ecc71'];
for(let i=0;i<n;i++){const e=document.createElement('div');e.className='sp';e.style.left=(10+Math.random()*80)+'%';e.style.top='-5px';e.style.background=cs[Math.floor(Math.random()*cs.length)];e.style.width=e.style.height=(2+Math.random()*4)+'px';e.style.animationDuration=(1.5+Math.random()*2)+'s';e.style.animationDelay=(Math.random()*0.5)+'s';c.appendChild(e);setTimeout(()=>e.remove(),4000);}
}
// ===== 启动 =====
function init() {
var hs = $('hStage');
if (hs) { const done = S.stars.filter(function(s){return s>0}).length; hs.textContent = Math.min(done+1,4)+'/4'; }
renderMap(); sync();
}
init();
</script>
</body>
</html>
+200
View File
@@ -0,0 +1,200 @@
const fs = require('fs');
const h = fs.readFileSync(__dirname + '\\index.html', 'utf8');
const m = h.match(/<script>([\s\S]*?)<\/script>/);
const js = m[1];
// Extract chapter data
const chapters = [
{ id: 1, name: '有理数', standard: '正负数·有理数分类·数轴·相反数·绝对值·加减法·乘除法·乘方·科学记数法·近似数·运算律' },
{ id: 2, name: '整式的加减', standard: '单项式·多项式·同类项·去括号·添括号·整式加减·降幂升幂' },
{ id: 3, name: '一元一次方程', standard: '等式性质·解方程·去分母·配套·工程·行程·销售·方案·比赛积分·利率' },
{ id: 4, name: '几何图形初步', standard: '立体图形·展开图·三视图·线段·射线·直线·中点·角·余角补角·角平分线·方位角·钟面角' }
];
console.log('╔══════════════════════════════════════════════╗');
console.log('║ 教育专家 · 严格内容审核报告 ║');
console.log('╚══════════════════════════════════════════════╝\n');
chapters.forEach(ch => {
const chStart = js.indexOf("{ id:" + ch.id + ", title:'" + ch.name + "'");
const chEnd = ch.id < 4 ? js.indexOf("{ id:" + (ch.id+1) + ", title:'") : js.indexOf("];", chStart);
const chContent = js.substring(chStart, chEnd);
console.log('▬'.repeat(50));
console.log('【' + ch.name + '】');
// Check KPs
const kps = chContent.match(/type:'kp', icon:'[^']+', title:'[^']+'/g);
if (kps) {
console.log('\n 知识点(KP):');
kps.forEach(kp => {
const title = kp.match(/title:'([^']+)'/)[1];
const icon = kp.match(/icon:'([^']+)'/)[1];
console.log(' ' + icon + ' ' + title);
});
}
// Check for explanatory content quality
const kjCount = (chContent.match(/class="kj"/g) || []).length;
const qjCount = (chContent.match(/class="qj"/g) || []).length;
const vidCount = (chContent.match(/<a href="https:\/\/www\.bilibili/g) || []).length;
console.log(' 口诀数: ' + kjCount + ' 巧解数: ' + qjCount + ' 视频数: ' + vidCount);
// Check question count and distribution
const ssQ = chContent.match(/type:'ss'/g);
const rvQ = chContent.match(/type:'review'/g);
const egQ = chContent.match(/type:'eg'/g);
console.log(' SS题:' + (ssQ ? ssQ.length : 0) + ' Review:' + (rvQ ? rvQ.length : 0) + ' EG:' + (egQ ? egQ.length : 0));
// Check for common explanation pitfalls
const hasMistake = chContent.includes('错误') || chContent.includes('易错');
const hasLifeExample = chContent.includes('温度计') || chContent.includes('银行卡') || chContent.includes('天平') || chContent.includes('快递');
const hasComparison = chContent.includes('对比') || chContent.includes('区别') || chContent.includes('vs');
console.log(' 易错提示:' + (hasMistake ? '✅' : '❌') + ' 生活例子:' + (hasLifeExample ? '✅' : '❌') + ' 对比说明:' + (hasComparison ? '✅' : '❌'));
});
// Check overall structure
console.log('\n\n▬'.repeat(50));
console.log('【总体审核】');
// 1. 内容完整性检查
console.log('\n 1. 知识点覆盖(课标要求 vs 实际覆盖):');
const allContent = js;
const standardTopics = [
['正负数', '正数', '基本概念'],
['有理数分类', '整数', '分类标准'],
['数轴', '原点', '数轴三要素'],
['相反数', '相反数', '符号相反'],
['绝对值', '绝对值', '距离'],
['加法法则', '同号相加', '运算规则'],
['减法法则', '减法', '减法法则'],
['乘法法则', '同号得正', '乘法'],
['除法法则', '除法', '除以'],
['乘方', '乘方', '幂运算'],
['科学记数法', '科学记数法', '大数表示'],
['近似数', '近似数', '精确度'],
['运算律', '交换律', '运算律'],
['有效数字', '有效数字', '精确位'],
['单项式', '单项式', '整式基础'],
['多项式', '多项式', '整式'],
['同类项', '同类项', '合并'],
['去括号', '去括号', '括号法则'],
['添括号', '添括号', '括号'],
['降幂排列', '降幂', '排列'],
['升幂排列', '升幂', '排列'],
['方程', '方程', '等式'],
['等式性质', '等式性质', '性质'],
['移项', '移项', '过桥'],
['去分母', '去分母', '公分母'],
['配套问题', '配套', '应用题'],
['工程问题', '工程', '效率'],
['行程问题', '行程', '路程'],
['销售问题', '进价', '利润'],
['方案选择', '方案', '比较'],
['比赛积分', '积分', '比赛'],
['存款利率', '利率', '利息'],
['立体图形', '棱柱', '立体'],
['展开图', '展开', '展开图'],
['线段', '线段', '端点'],
['射线', '射线', '端点'],
['直线', '直线', '无限'],
['中点', '中点', '中点'],
['角', '角', '角度'],
['余角', '余角', '90°'],
['补角', '补角', '180°'],
['角平分线', '平分', '角平分线'],
['方位角', '北偏', '方位'],
['钟面角', '时针', '钟面'],
];
let covered = 0, missing = [];
standardTopics.forEach(t => {
if (allContent.includes(t[1])) { covered++; }
else { missing.push(t[0]); }
});
console.log(' 覆盖: ' + covered + '/' + standardTopics.length);
if (missing.length > 0) console.log(' 缺失: ' + missing.join(', '));
// 2. 讲解生动性
console.log('\n 2. 讲解生动性:');
const metaphors = [
['照镜子', '相反数'],
['天平', '方程'],
['快递', '去括号'],
['双胞胎', '同类项'],
['单词/句子', '整式'],
['公路', '数轴'],
['温度计', '正负数'],
['手电筒', '射线'],
['土豆/土豆片', '立体vs平面'],
];
let metaphorCount = 0;
metaphors.forEach(m => {
if (allContent.includes(m[0])) { metaphorCount++; console.log(' ✅ ' + m[0] + ' → ' + m[1]); }
else { console.log(' ❌ ' + m[0] + '(未使用)'); }
});
// 3. 易错点提示
console.log('\n 3. 易错点/常见错误提示:');
const tips = [
['π是常数', 'π是数字'],
['-1⁴≠(-1)⁴', '1⁴'],
['平均速度误区', '平均速度'],
['去分母漏乘', '漏乘'],
['过桥要变号', '过桥'],
['负数比较大小', '绝对值大的反而小'],
];
tips.forEach(t => {
if (allContent.includes(t[1])) console.log(' ✅ ' + t[0]);
else console.log(' ❌ ' + t[0] + '(未提示)');
});
// 4. 练习多样性
console.log('\n 4. 练习多样性:');
const calcQ = (allContent.match(/计算/g) || []).length;
const conceptQ = (allContent.match(/说法正确的是/g) || []).length;
const wordQ = (allContent.match(/km|元|小时|分钟|天/g) || []).length;
const graphQ = (allContent.match(/Geo\./g) || []).length;
console.log(' 计算题引用:' + calcQ + '次 概念判断题:' + conceptQ + '次');
console.log(' 应用题情境:' + wordQ + '次 Geo图形题:' + graphQ + '个');
// 5. 难度梯度
console.log('\n 5. 难度梯度(重点→核心→高频→综合):');
const tags = {};
const allTags = js.match(/tag:'[^']+'/g);
if (allTags) {
allTags.forEach(t => {
const tag = t.split("'")[1];
tags[tag] = (tags[tag] || 0) + 1;
});
Object.keys(tags).forEach(t => console.log(' ' + t + ': ' + tags[t] + '题'));
}
// 6. 口诀有效性
console.log('\n 6. 口诀可记忆性:');
const rhymes = [
'零上为正零下为负',
'数轴一条线原点在中间',
'相反数是照镜子',
'同号相加符号不变',
'奇次方符号不变',
'去括号等于拆快递',
'移项过桥要变号',
'线段两个点射线一个点',
'一四一、二三一、二二二、三三',
'方位角互看',
];
rhymes.forEach(r => {
if (allContent.includes(r.substring(0, 6))) console.log(' ✅ ' + r.substring(0, 20) + '...');
else console.log(' ❌ ' + r.substring(0, 20) + '...(未找到)');
});
console.log('\n' + '═'.repeat(50));
console.log('审核结论:');
if (covered === standardTopics.length && metaphorCount >= 7) {
console.log('✅ 知识点完整,讲解生动,符合教学标准');
} else {
console.log('⚠️ 存在以下问题:');
if (missing.length > 0) console.log(' - 知识点缺失: ' + missing.join(', '));
if (metaphorCount < 7) console.log(' - 比喻/类比不足(仅' + metaphorCount + '/9');
}
+77
View File
@@ -0,0 +1,77 @@
const { chromium } = require('playwright');
const htmlPath = 'C:\\Users\\然然小可爱\\Desktop\\数学乐园\\index.html';
const fileUrl = 'file:///' + htmlPath.replace(/\\/g, '/');
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
page.on('pageerror', e => console.log(' JS_ERROR:', e.message));
await page.goto(fileUrl, { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(1000);
console.log('=== 测试:EG例题流程 ===');
// 进入Ch1
const cp0 = await page.$('.cp[data-idx="0"]');
const b = await cp0.boundingBox();
await page.mouse.click(b.x + b.width/2, b.y + b.height/2);
await page.waitForTimeout(300);
// 点 "发车" 进入KP
await page.click('.act.show');
await page.waitForTimeout(200);
// KP1继续、KP2继续、KP3继续 = 3次
for (let i = 0; i < 3; i++) {
await page.click('.act.show');
await page.waitForTimeout(100);
}
// 到达EG
const egTitle = await page.$eval('.eg-title', e => e.textContent).catch(() => 'NO_EG');
console.log('EG标题:', egTitle);
// 检查EG初始显示几个步骤
const stepCnt = await page.$$eval('.eg-step', els => els.length);
console.log('初始步骤数:', stepCnt);
// 检查按钮类型
const btnId = await page.evaluate(() => {
const btn = document.querySelector('.eg-btn, .act.show');
return btn ? (btn.id || btn.className) : 'none';
});
console.log('初始按钮:', btnId);
// 逐步骤点击 "下一步"
let clickCount = 0;
while (true) {
const egBtn = await page.$('#egBtn');
if (!egBtn) break;
await egBtn.click();
await page.waitForTimeout(150);
clickCount++;
const steps = await page.$$eval('.eg-step.show', els => els.length);
console.log('点击' + clickCount + '次后, 显示步骤:', steps, '/' + 5);
}
console.log('总"下一步"点击:', clickCount);
// 现在应该出现 "继续" 按钮
const hasContinue = await page.$('.act.show');
console.log('有"继续"按钮:', hasContinue ? '✅' : '❌');
// 点击"继续"
if (hasContinue) {
await hasContinue.click();
await page.waitForTimeout(300);
const nextContent = await page.$eval('#cGl', e => e.textContent).catch(() => '');
console.log('继续后内容:', nextContent);
// 检查是否离开了EG
const isStillEG = await page.$('.eg-title');
console.log('是否还在EG页:', isStillEG ? '❌ 仍在该页面' : '✅ 已前进');
}
await browser.close();
})().catch(e => { console.log('失败:', e.message); process.exit(1); });
+52
View File
@@ -0,0 +1,52 @@
const { chromium } = require('playwright');
const path = 'C:\\Users\\然然小可爱\\Desktop\\数学乐园\\index.html';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
page.on('pageerror', e => console.log(' ERROR:', e.message));
await page.goto('file:///' + path.replace(/\\/g, '/'));
await page.waitForTimeout(1000);
// Unlock all
await page.evaluate(() => { S.stars = [3,3,3,3]; sv(); renderMap(); });
await page.waitForTimeout(300);
// Navigate via goChap
await page.evaluate(() => goChap(3));
await page.waitForTimeout(500);
const gl = await page.$eval('#cGl', e => e.textContent).catch(() => 'ERR');
console.log('当前段:', gl);
// Walk through KPs
for (let step = 0; step < 7; step++) {
const act = await page.$('.act.show');
if (act) await act.click(); await page.waitForTimeout(500);
const label = await page.$eval('#cGl', e => e.textContent).catch(() => '?');
const svgAnims = await page.evaluate(() => {
const svgs = document.querySelectorAll('#cCo svg');
let n = 0;
svgs.forEach(s => { if (s.innerHTML.includes('animate')) n++; });
return n;
});
console.log((step+1) + '. ' + label + ' SVG动画:' + svgAnims + '个');
}
// Final content check
const body = await page.evaluate(() => document.body.innerHTML);
const checks = [
['展开图滑入', body.includes('展开图演示')],
['角度弧线', body.includes('∠=')],
['钟面8:30', body.includes('8:30')],
['射线延伸', body.includes('x2')],
['中点滑动', body.includes('cx"')],
];
console.log('\n动画内容:');
checks.forEach(c => console.log(' ' + (c[1] ? '✅' : '❌') + ' ' + c[0]));
await browser.close();
})().catch(e => console.log('失败:', e.message));
+117
View File
@@ -0,0 +1,117 @@
const { chromium } = require('playwright');
const htmlPath = 'C:\\Users\\然然小可爱\\Desktop\\数学乐园\\index.html';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
let p = 0, f = 0;
function chk(name, cond, note) {
if (cond) { p++; console.log(' ✅', name); }
else { f++; console.log(' ❌', name, note||''); }
}
await page.goto('file:///' + htmlPath.replace(/\\/g, '/'));
await page.waitForTimeout(1000);
// ===== 1. 主页 =====
console.log('【1. 主页】');
chk('标题', await page.title() === 'MV1 赛道数学');
chk('CP数', (await page.$$('.cp')).length === 4);
chk('HUD', await page.$eval('#hStage', e => e.textContent) === '1/4');
// ===== 2. 进入Ch1推进 =====
console.log('\n【2. 学习推进】');
const cp = await page.$('.cp[data-idx="0"]');
const b = await cp.boundingBox();
await page.mouse.click(b.x + b.width/2, b.y + b.height/2);
await page.waitForTimeout(500);
// 推进函数
async function adv() {
const act = await page.$('.act.show');
if (act) { await act.click(); await page.waitForTimeout(350); return true; }
return false;
}
await adv(); chk('KP1正负数', (await page.$eval('#cGl', e => e.textContent)).includes('正数'));
await adv(); chk('KP2数轴', (await page.$eval('#cGl', e => e.textContent)).includes('数轴'));
await adv(); chk('KP3相反数', (await page.$eval('#cGl', e => e.textContent)).includes('相反'));
// EG分步
for (let i = 0; i < 5; i++) {
const eb = await page.$('#egBtn');
if (!eb) break;
await eb.click(); await page.waitForTimeout(100);
}
await adv(); chk('KP4运算', (await page.$eval('#cGl', e => e.textContent)).includes('运算'));
await adv(); chk('KP5乘方', (await page.$eval('#cGl', e => e.textContent)).includes('乘方'));
await adv(); chk('分站赛入口', await page.$('#stgStart') !== null);
// ===== 3. 答题 =====
console.log('\n【3. 答题】');
await page.click('#stgStart'); await page.waitForTimeout(300);
chk('题目显示', await page.$('.qt') !== null);
chk('4选项', (await page.$$('.opt')).length === 4);
// 答对
const c1 = await page.evaluate(() => SS[0].segs[8].correct);
await (await page.$$('.opt'))[c1].click(); await page.waitForTimeout(300);
chk('答对反馈', await page.$('.qfb.pass') !== null);
await page.click('.act.show'); await page.waitForTimeout(200);
// 答错
const c2 = await page.evaluate(() => SS[0].segs[9].correct);
await (await page.$$('.opt'))[c2 === 0 ? 1 : 0].click(); await page.waitForTimeout(300);
chk('答错反馈', await page.$('.qfb.fail') !== null);
chk('错题角标', await page.$eval('#hintBadge', e => parseInt(e.textContent) > 0));
// ===== 4. 答题地图 =====
console.log('\n【4. 答题地图】');
await page.click('.act.show'); await page.waitForTimeout(200);
const mk = await page.$('#bkMapBtn');
if (mk && (await mk.isVisible())) {
await mk.click(); await page.waitForTimeout(300);
chk('地图显示', await page.$('#mGrid') !== null);
const cells = await page.$$('.qCell');
chk('题目格', cells.length > 0, cells.length + '个');
chk('正确标记', await page.$('.qCell.correct') !== null);
chk('错误标记', await page.$('.qCell.wrong') !== null);
if (cells.length > 0) { await cells[3].click(); await page.waitForTimeout(300); chk('跳转', await page.$('.qt') !== null); }
}
// ===== 5. 错题集 =====
console.log('\n【5. 错题集】');
// 先回章节页再回主页
const bm = await page.$('#bkChapM');
if (bm && (await bm.isVisible())) { await bm.click(); await page.waitForTimeout(200); }
await page.click('#bkMap'); await page.waitForTimeout(200);
chk('回到主页', (await page.evaluate(() => document.querySelector('.pg.act')?.id)) === 'pH');
await page.click('#wrongBtn'); await page.waitForTimeout(300);
chk('错题集显示', (await page.evaluate(() => document.querySelector('.pg.act')?.id)) === 'pW');
chk('重做按钮', await page.$('.wrDo') !== null);
chk('移除按钮', await page.$('.wrRm') !== null);
// ===== 6. 内容完整性 =====
console.log('\n【6. 内容覆盖】');
const txt = await page.evaluate(() => document.body.innerText);
const items = [
['有理数','有理数'],['正负数','正数'],['数轴','数轴'],['相反数','相反数'],
['绝对值','绝对值'],['倒数','倒数'],['乘方','乘方'],['科学记数法','科学记数法'],
['单项式','单项式'],['多项式','多项式'],['同类项','同类项'],['去括号','去括号'],
['降幂','降幂'],['升幂','升幂'],['等式性质','等式性质'],['去分母','去分母'],
['配套','螺栓'],['相遇','相向'],['追及','追及'],['比赛积分','积分'],
['利率','利率'],['展开图','一四一'],['中点模型','中点'],['角平分线','平分'],
['分类讨论','分类讨论'],['有效数字','有效数字'],['方程','方程'],
];
items.forEach(i => chk(i[0], txt.includes(i[1])));
console.log(`\n通过: ${p}/${p+f} JS错误: ${errors.length}`);
errors.forEach(e => console.log(' Error:', e));
await browser.close();
})().catch(e => { console.log('异常:', e.message); process.exit(1); });