<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>质数大冒险(有声版) | 五年级奥数</title>
<style>
:root {
--primary: #FF9F1C;
--secondary: #2EC4B6;
--accent: #E71D36;
--bg: #FDFFFC;
--dark: #011627;
--gray: #f0f4f8;
--success: #4CAF50;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--dark);
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* 顶部导航 */
header {
background-color: var(--secondary);
color: white;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 10;
flex-shrink: 0;
}
h1 {
margin: 0;
font-size: 1.2rem;
font-weight: 800;
letter-spacing: 1px;
}
.sound-toggle {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
}
/* 底部导航栏 */
nav {
display: flex;
justify-content: space-around;
background: white;
padding: 10px 0;
border-top: 1px solid #eee;
flex-shrink: 0;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-btn {
background: none;
border: none;
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.8rem;
color: #888;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.nav-btn span {
font-size: 1.5rem;
margin-bottom: 4px;
}
.nav-btn.active {
color: var(--secondary);
transform: scale(1.1);
}
/* 主内容区域 */
main {
flex-grow: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.card {
background: white;
border-radius: 20px;
padding: 20px;
width: 100%;
max-width: 500px;
box-shadow: 0 10px 25px rgba(0,0,0,0.05);
margin-bottom: 20px;
border: 2px solid var(--gray);
animation: slideUp 0.4s ease-out;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
h2 {
color: var(--secondary);
margin-top: 0;
font-size: 1.4rem;
}
p {
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 15px;
}
.highlight {
color: var(--accent);
font-weight: bold;
font-size: 1.2rem;
}
/* 按钮样式 */
.btn-primary {
background-color: var(--primary);
color: white;
border: none;
padding: 15px 30px;
font-size: 1.2rem;
border-radius: 50px;
font-weight: bold;
width: 100%;
max-width: 300px;
cursor: pointer;
box-shadow: 0 4px 0 #d6810b;
transition: transform 0.1s;
margin-top: auto;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-primary:active {
transform: translateY(4px);
box-shadow: none;
}
.btn-primary:disabled {
background-color: #ccc;
box-shadow: none;
cursor: not-allowed;
}
/* 100数字网格 */
.grid-100 {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 4px;
margin: 15px 0;
}
.num-cell {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: #f0f0f0;
font-size: 0.8rem;
font-weight: bold;
transition: all 0.3s;
}
.num-cell.prime {
background-color: var(--primary);
color: white;
transform: scale(1.1);
z-index: 1;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.num-cell.composite {
background-color: #e0e0e0;
color: #bbb;
transform: scale(0.9);
opacity: 0.5;
}
/* 短除法样式 */
.division-container {
font-family: 'Courier New', monospace;
font-size: 1.8rem;
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
}
.division-step {
display: flex;
align-items: flex-end;
opacity: 0;
transform: translateX(-10px);
transition: all 0.5s;
}
.division-step.visible {
opacity: 1;
transform: translateX(0);
}
.divisor {
padding-right: 10px;
border-right: 2px solid var(--dark);
border-bottom: 2px solid var(--dark);
padding-bottom: 5px;
margin-bottom: 5px;
color: var(--accent);
min-width: 40px;
text-align: right;
}
.dividend {
padding-left: 10px;
border-bottom: 2px solid var(--dark);
padding-bottom: 5px;
margin-bottom: 5px;
min-width: 60px;
}
.last-quotient {
padding-left: 50px;
color: var(--secondary);
font-weight: bold;
}
/* 闯关题库 */
.quiz-options {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.option-btn {
background: white;
border: 2px solid var(--gray);
padding: 15px;
border-radius: 12px;
text-align: left;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.option-btn.correct {
background-color: #d1fae5;
border-color: var(--success);
color: var(--success);
}
.option-btn.wrong {
background-color: #fee2e2;
border-color: var(--accent);
color: var(--accent);
}
.feedback {
margin-top: 15px;
padding: 15px;
border-radius: 10px;
background: #f0f9ff;
border-left: 5px solid var(--secondary);
font-size: 1rem;
display: none;
animation: fadeIn 0.3s;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* 方块可视化 */
.block-visual {
display: flex;
flex-wrap: wrap;
gap: 2px;
justify-content: center;
margin: 10px 0;
}
.block {
width: 30px;
height: 30px;
background: var(--secondary);
border-radius: 4px;
}
.block-row {
display: flex;
gap: 2px;
margin-bottom: 2px;
}
/* 启动遮罩层 */
#start-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--secondary);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
text-align: center;
padding: 20px;
}
#start-overlay h1 {
font-size: 2.5rem;
margin-bottom: 20px;
}
#start-overlay button {
font-size: 1.5rem;
padding: 15px 40px;
background: white;
color: var(--secondary);
border: none;
border-radius: 50px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<!-- 启动遮罩层,用于激活音频环境 -->
<div id="start-overlay">
<h1>🚀 质数大冒险</h1>
<p style="margin-bottom: 40px; font-size: 1.2rem;">准备好打开声音<br>开始挑战了吗?</p>
<button onclick="startApp()">开始学习 ▶️</button>
</div>
<header>
<h1>🚀 质数大冒险</h1>
<button class="sound-toggle" onclick="toggleSound()">🔊 声音: 开</button>
</header>
<main id="app">
<!-- 内容区 -->
</main>
<nav>
<button class="nav-btn active" onclick="router('concept')">
<span>💡</span>概念
</button>
<button class="nav-btn" onclick="router('sieve')">
<span>🔢</span>100表
</button>
<button class="nav-btn" onclick="router('division')">
<span>✏️</span>短除法
</button>
<button class="nav-btn" onclick="router('tricks')">
<span>✨</span>技巧
</button>
<button class="nav-btn" onclick="router('quiz')">
<span>🏆</span>闯关
</button>
</nav>
<script>
// --- Audio Engine (No external files) ---
const SoundFX = {
ctx: null,
enabled: true,
init: function() {
// Initialize AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
},
playTone: function(freq, type, duration, startTime = 0) {
if (!this.enabled || !this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + startTime);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime + startTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + startTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(this.ctx.currentTime + startTime);
osc.stop(this.ctx.currentTime + startTime + duration);
},
// Sounds
click: function() {
// Short high pop
this.playTone(600, 'sine', 0.1);
},
pop: function() {
// Bubble sound for sieve
if (!this.enabled || !this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.setValueAtTime(400, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
},
correct: function() {
// Happy major triad (C E G)
this.playTone(523.25, 'sine', 0.1, 0); // C5
this.playTone(659.25, 'sine', 0.1, 0.1); // E5
this.playTone(783.99, 'sine', 0.3, 0.2); // G5
},
wrong: function() {
// Sad low buzz
this.playTone(150, 'sawtooth', 0.4);
this.playTone(140, 'sawtooth', 0.4, 0.1);
},
magic: function() {
// Sparkle sound
for(let i=0; i<10; i++) {
this.playTone(800 + i*100, 'sine', 0.05, i*0.03);
}
},
// TTS (Text to Speech)
speak: function(text) {
if (!this.enabled || !window.speechSynthesis) return;
// Cancel previous speech
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.rate = 1.0;
utterance.pitch = 1.1; // Slightly higher pitch for "friendly teacher" voice
window.speechSynthesis.speak(utterance);
}
};
function startApp() {
SoundFX.init();
// Unlock audio context for iOS
if (SoundFX.ctx.state === 'suspended') {
SoundFX.ctx.resume();
}
document.getElementById('start-overlay').style.display = 'none';
SoundFX.playTone(440, 'sine', 0.1); // Test sound
SoundFX.speak("欢迎来到质数大冒险!");
init();
}
function toggleSound() {
SoundFX.enabled = !SoundFX.enabled;
const btn = document.querySelector('.sound-toggle');
btn.innerText = SoundFX.enabled ? "🔊 声音: 开" : "🔇 声音: 关";
btn.style.background = SoundFX.enabled ? "rgba(255,255,255,0.2)" : "rgba(255,0,0,0.5)";
}
// --- Content Data ---
const contentData = {
concept: {
title: "什么是质数与合数?",
steps: [
{
text: "让我们来玩个拼图游戏!<br>如果我们有 <b>6</b> 个小方块,能拼成几种长方形?",
speak: "如果我们有6个小方块,能拼成几种长方形?",
visual: `<div class="block-visual">
<div style="display:flex; flex-direction:column; gap:2px; margin-right:10px;">
<div class="block-row"><div class="block"></div><div class="block"></div><div class="block"></div></div>
<div class="block-row"><div class="block"></div><div class="block"></div><div class="block"></div></div>
<p style="font-size:0.8rem; text-align:center; color:#666">2行3列</p>
</div>
<div style="display:flex; flex-direction:column; gap:2px;">
<div class="block-row"><div class="block"></div><div class="block"></div><div class="block"></div><div class="block"></div><div class="block"></div><div class="block"></div></div>
<p style="font-size:0.8rem; text-align:center; color:#666">1行6列</p>
</div>
</div>`,
note: "6 可以写成 2×3,也可以写成 1×6。因数有:1, 2, 3, 6。它好'合'群!"
},
{
text: "那如果我们只有 <b>5</b> 个小方块呢?",
speak: "那如果我们只有5个小方块呢?",
visual: `<div class="block-visual">
<div style="display:flex; flex-direction:column; gap:2px;">
<div class="block-row"><div class="block" style="background:var(--primary)"></div><div class="block" style="background:var(--primary)"></div><div class="block" style="background:var(--primary)"></div><div class="block" style="background:var(--primary)"></div><div class="block" style="background:var(--primary)"></div></div>
<p style="font-size:0.8rem; text-align:center; color:#666">只能拼成 1行5列</p>
</div>
</div>`,
note: "5 只能写成 1×5。因数只有:1 和它自己。这就是<span class='highlight'>质数</span>!"
},
{
text: "📝 <b>牢记口诀:</b>",
speak: "请牢记:只有1和它本身两个因数的,叫质数。",
visual: `<div style="background:#fff3cd; padding:15px; border-radius:10px; border:2px solid #ffeeba;">
只有 1 和它本身两个因数 👉 <b>质数</b><br><br>
除了 1 和它本身还有别的因数 👉 <b>合数</b><br><br>
⚠️ <b>1 既不是质数也不是合数!</b>
</div>`,
note: "准备好去寻找100以内的质数了吗?"
}
]
},
sieve: {
title: "抓捕100以内的质数",
intro: "我们用'筛子'把合数都筛掉,剩下的就是质数!点击下方按钮开始操作。",
steps: [
{ action: "init", text: "这是1到100的数字表。", speak: "这是1到100的数字表。" },
{ action: "mark1", text: "首先,<b>1</b> 不是质数也不是合数,把它扔掉!(灰色)", speak: "1不是质数也不是合数,扔掉!" },
{ action: "mark2", text: "<b>2</b> 是最小的质数!<br>但是,2的倍数(4,6,8...)肯定都有因数2,都是合数,划掉!", speak: "2是质数,但2的倍数都要划掉。" },
{ action: "mark3", text: "<b>3</b> 是质数!<br>3的倍数(6,9,12...)都是合数,划掉!", speak: "3是质数,划掉3的倍数。" },
{ action: "mark5", text: "4已经被划掉了。下一个是 <b>5</b>!<br>5的倍数(10,15,25...)都是合数,划掉!", speak: "5是质数,划掉5的倍数。" },
{ action: "mark7", text: "<b>7</b> 是质数!<br>7的倍数(14,21,49...)划掉!", speak: "7是质数,划掉7的倍数。" },
{ action: "reveal", text: "剩下的数字都没有被划掉,它们就是质数!<br><span class='highlight'>一共有25个,背诵口诀:</span><br>二三五七和十一,<br>十三后面是十七,<br>十九二三二十九...", speak: "剩下的都是质数,一共有25个,要背下来哦!" }
]
},
division: {
title: "分解质因数:短除法",
intro: "要把一个大合数拆成一堆小质数相乘,我们用<b>短除法</b>。",
steps: [
{
text: "例题:把 <b>60</b> 分解质因数。",
speak: "我们把60分解质因数,先写个厂字框。",
renderVal: 60,
divisors: [],
quotients: [60],
highlight: "写下60,画个厂字框。"
},
{
text: "想一个能整除60的最小质数... 是 <b>2</b>!",
speak: "用最小质数2去除,60除以2等于30。",
renderVal: 60,
divisors: [2],
quotients: [60, 30],
highlight: "60 ÷ 2 = 30"
},
{
text: "30 还是合数,继续除以最小质数 <b>2</b>。",
speak: "30还是合数,继续除以2,等于15。",
renderVal: 60,
divisors: [2, 2],
quotients: [60, 30, 15],
highlight: "30 ÷ 2 = 15"
},
{
text: "15 不能被2整除了。试试下一个质数 <b>3</b>。",
speak: "15不能被2整除,我们试一下3。15除以3等于5。",
renderVal: 60,
divisors: [2, 2, 3],
quotients: [60, 30, 15, 5],
highlight: "15 ÷ 3 = 5"
},
{
text: "<b>5</b> 是质数!不用再除了。大功告成!",
speak: "5是质数,不用再除了。分解完成!",
renderVal: 60,
divisors: [2, 2, 3],
quotients: [60, 30, 15, 5],
done: true,
highlight: "结果:60 = 2 × 2 × 3 × 5"
}
]
},
tricks: {
title: "⚡️ 速算技巧与特殊分解",
steps: [
{
title: "特工代号 007",
content: "看到 <b>1001</b> 这个数字,要马上想到:<br><br><span class='highlight' style='font-size:1.5rem'>1001 = 7 × 11 × 13</span><br><br>记忆法:7-11便利店,或者特工007/11/13。",
speak: "记住1001等于7乘11乘13。"
},
{
title: "光棍的秘密",
content: "看到 <b>111</b> (三个1),加起来是3,所以一定能被3整除。<br><br><span class='highlight' style='font-size:1.5rem'>111 = 3 × 37</span>",
speak: "111等于3乘以37。"
},
{
title: "年份题常客",
content: "有些年份是质数,有些是合数。奥数题常考!<br><br><b>2021</b> = 43 × 47 (两个质数连续)<br><b>101</b> 是三位中最小的质数。",
speak: "注意年份题,101是三位数里最小的质数。"
}
]
},
quiz: [
{
q: "1. 下列哪个数字是质数?",
options: ["9", "1", "2", "15"],
ans: 2,
expl: "1不是质数;9=3×3;15=3×5。只有2是质数!"
},
{
q: "2. 100以内的质数一共有多少个?",
options: ["20个", "24个", "25个", "26个"],
ans: 2,
expl: "一定要背下来哦,100以内共有25个质数。"
},
{
q: "3. 把 30 分解质因数,正确的是?",
options: ["30 = 5 × 6", "30 = 1 × 2 × 3 × 5", "30 = 2 × 3 × 5", "30 = 2 + 3 + 25"],
ans: 2,
expl: "A中6是合数;B中1不能写进去;D是加法。正确是 2×3×5。"
},
{
q: "4. 111 的质因数分解是?",
options: ["111 = 3 × 37", "111 = 1 × 111", "111 = 3 × 31", "111是质数"],
ans: 0,
expl: "1+1+1=3,能被3整除。111 ÷ 3 = 37。"
},
{
q: "5. 两个质数的和是 10,这两个质数的积是多少?",
options: ["16", "21", "25", "24"],
ans: 1,
expl: "想:几加几等于10?3+7=10,都是质数。3×7=21。"
}
]
};
// --- State Management ---
let currentState = {
section: 'concept',
step: 0,
quizScore: 0
};
function init() {
router('concept');
}
function router(sectionName) {
SoundFX.click();
currentState.section = sectionName;
currentState.step = 0;
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
const activeBtn = document.querySelector(`.nav-btn[onclick="router('${sectionName}')"]`);
if(activeBtn) activeBtn.classList.add('active');
render();
}
function render() {
const app = document.getElementById('app');
app.innerHTML = '';
const data = contentData[currentState.section];
const titleEl = document.createElement('h2');
titleEl.innerHTML = data.title || "闯关练习";
app.appendChild(titleEl);
// Render functions
if (currentState.section === 'concept') renderConcept(app, data);
else if (currentState.section === 'sieve') renderSieve(app, data);
else if (currentState.section === 'division') renderDivision(app, data);
else if (currentState.section === 'tricks') renderTricks(app, data);
else if (currentState.section === 'quiz') renderQuiz(app, data);
}
// --- Renderers ---
function renderConcept(container, data) {
const stepData = data.steps[currentState.step];
// Speak instruction if it's the first render of this step
if(stepData.speak) SoundFX.speak(stepData.speak);
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
${stepData.visual}
<p>${stepData.text}</p>
${stepData.note ? `<div style="background:#f0f4f8; padding:10px; border-radius:8px; font-size:0.9rem; color:#555">💡 ${stepData.note}</div>` : ''}
`;
container.appendChild(card);
const btn = document.createElement('button');
btn.className = 'btn-primary';
btn.innerHTML = currentState.step < data.steps.length - 1 ? "下一步 👉" : "去学下一章 🚀";
btn.onclick = () => {
SoundFX.click();
if (currentState.step < data.steps.length - 1) {
currentState.step++;
render();
} else {
router('sieve');
}
};
container.appendChild(btn);
}
function renderSieve(container, data) {
// Grid
const grid = document.createElement('div');
grid.className = 'grid-100 card';
for(let i=1; i<=100; i++) {
const cell = document.createElement('div');
cell.className = 'num-cell';
cell.id = `num-${i}`;
cell.innerText = i;
grid.appendChild(cell);
}
container.appendChild(grid);
// Text
const stepData = data.steps[currentState.step];
if(stepData.speak) SoundFX.speak(stepData.speak);
const info = document.createElement('div');
info.className = 'card';
info.innerHTML = `<p id="sieve-text">${stepData.text}</p>`;
container.appendChild(info);
// Visual Logic with Sound
setTimeout(() => {
const step = currentState.step;
// Re-apply previous states lightly
if(step >= 1) document.getElementById('num-1').classList.add('composite');
const markMultiples = (base, isCurrentStep) => {
const baseEl = document.getElementById(`num-${base}`);
baseEl.classList.add('prime');
if(isCurrentStep) {
// Animation with sound effect cascade
let count = 0;
for(let j=base*2; j<=100; j+=base) {
setTimeout(() => {
const el = document.getElementById(`num-${j}`);
if(!el.classList.contains('composite')){
el.classList.add('composite');
// Only play pop sound for first few to avoid chaos
if(count < 8) SoundFX.pop();
}
}, count * 50); // Cascade delay
count++;
}
} else {
// Just mark statically for previous steps
for(let j=base*2; j<=100; j+=base) {
document.getElementById(`num-${j}`).classList.add('composite');
}
}
};
if (step >= 2) markMultiples(2, step === 2);
if (step >= 3) markMultiples(3, step === 3);
if (step >= 4) markMultiples(5, step === 4);
if (step >= 5) markMultiples(7, step === 5);
if (step === 6) {
// Reveal all
SoundFX.magic();
const primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97];
primes.forEach(p => document.getElementById(`num-${p}`).classList.add('prime'));
}
}, 100);
const btn = document.createElement('button');
btn.className = 'btn-primary';
btn.innerHTML = currentState.step < data.steps.length - 1 ? "下一步 🪄" : "去学短除法 ✏️";
btn.onclick = () => {
SoundFX.click();
if (currentState.step < data.steps.length - 1) {
currentState.step++;
render();
} else {
router('division');
}
};
container.appendChild(btn);
}
function renderDivision(container, data) {
const stepData = data.steps[currentState.step];
if(stepData.speak) SoundFX.speak(stepData.speak);
const card = document.createElement('div');
card.className = 'card';
let divisionHTML = `<div class="division-container">`;
for (let i = 0; i < stepData.divisors.length; i++) {
divisionHTML += `
<div class="division-step visible">
<div class="divisor">${stepData.divisors[i]}</div>
<div class="dividend">${stepData.quotients[i]}</div>
</div>
`;
}
const lastQ = stepData.quotients[stepData.quotients.length -1];
divisionHTML += `
<div class="division-step visible" style="margin-top:5px;">
<div class="last-quotient">${lastQ}</div>
</div>
`;
divisionHTML += `</div>`;
card.innerHTML = divisionHTML + `<p class="highlight" style="text-align:center; font-size:1rem; margin-top:10px;">${stepData.highlight}</p><p>${stepData.text}</p>`;
container.appendChild(card);
const btn = document.createElement('button');
btn.className = 'btn-primary';
btn.innerHTML = currentState.step < data.steps.length - 1 ? "除一下 ➗" : "看特殊技巧 ✨";
btn.onclick = () => {
SoundFX.click();
if (currentState.step < data.steps.length - 1) {
currentState.step++;
render();
} else {
router('tricks');
}
};
container.appendChild(btn);
}
function renderTricks(container, data) {
const steps = data.steps;
if(currentState.step === 0) SoundFX.speak("这些特殊数字的分解,一定要记住哦。");
steps.forEach((trick, index) => {
const card = document.createElement('div');
card.className = 'card';
card.style.animationDelay = `${index * 0.1}s`;
card.innerHTML = `<h3>${trick.title}</h3><p>${trick.content}</p>`;
card.onclick = () => {
SoundFX.click();
if(trick.speak) SoundFX.speak(trick.speak);
}; // Click to read
container.appendChild(card);
});
const btn = document.createElement('button');
btn.className = 'btn-primary';
btn.innerText = "开始闯关!🏆";
btn.onclick = () => router('quiz');
container.appendChild(btn);
}
function renderQuiz(container, data) {
const qIndex = currentState.step;
if (qIndex >= data.length) {
SoundFX.magic();
SoundFX.speak("恭喜通关!你太棒了!");
container.innerHTML = `
<div class="card" style="text-align:center; padding:40px;">
<h1 style="font-size:3rem;">🎉</h1>
<h2>恭喜通关!</h2>
<p>你已经是质数小专家了!</p>
<button class="btn-primary" onclick="init()">再玩一次 🔄</button>
</div>
`;
return;
}
const qData = data[qIndex];
SoundFX.speak(qData.q);
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `<p style="font-weight:bold; font-size:1.2rem;">${qData.q}</p>`;
const optionsDiv = document.createElement('div');
optionsDiv.className = 'quiz-options';
const feedbackDiv = document.createElement('div');
feedbackDiv.className = 'feedback';
let answered = false;
qData.options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.innerText = opt;
btn.onclick = () => {
if(answered) return;
answered = true;
if (idx === qData.ans) {
SoundFX.correct();
SoundFX.speak("回答正确!");
btn.classList.add('correct');
feedbackDiv.innerHTML = `<b>✅ 太棒了!</b><br>${qData.expl}`;
feedbackDiv.style.borderLeftColor = "var(--success)";
nextBtn.disabled = false;
} else {
SoundFX.wrong();
SoundFX.speak("哎呀,不对哦。");
btn.classList.add('wrong');
feedbackDiv.innerHTML = `<b>❌ 哎呀,不对哦。</b><br>${qData.expl}`;
feedbackDiv.style.borderLeftColor = "var(--accent)";
nextBtn.disabled = false;
}
feedbackDiv.style.display = 'block';
};
optionsDiv.appendChild(btn);
});
card.appendChild(optionsDiv);
card.appendChild(feedbackDiv);
container.appendChild(card);
const nextBtn = document.createElement('button');
nextBtn.className = 'btn-primary';
nextBtn.innerText = "下一题 👉";
nextBtn.disabled = true;
nextBtn.onclick = () => {
SoundFX.click();
currentState.step++;
render();
};
container.appendChild(nextBtn);
}
</script>
</body>
</html>
💡 这段代码完全由 gemini 生成。
登录后可复制完整代码