Код · 交通工具与动词搭配
CC BY 4.0 · укажите @Sauyeon · v1
Берите, делайте ремикс, учите с ним
Скопируйте код ниже в ChatGPT, Claude, Gemini, Grok — любой удобный инструмент — адаптируйте для своего класса и опубликуйте ремикс здесь с указанием авторства.
Заготовка подсказки для ИИ
Edit this classroom game for interactive whiteboards. Keep large touch targets and whole-class play. Tracking, logins, personal-data collection, and remote API calls are prohibited. Preserve its single-HTML structure. Preserve the educational goal. Original creator: @Sauyeon on ClassBoard Games (CC BY 4.0).
game-5.html · 25.3 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交通工具接力|교통수단 릴레이</title>
<style>
:root{
--bg:#f4f8f6; --panel:#ffffff; --green:#4f8f75; --green2:#eaf5ef;
--blue:#4f7fa8; --blue2:#edf5fb; --orange:#e78a4a; --red:#d85b5b;
--ink:#17221d; --muted:#65736c; --line:#dbe6df; --shadow:0 8px 24px rgba(34,68,51,.08);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0; font-family:"Malgun Gothic","Noto Sans KR","Noto Sans SC","Microsoft YaHei",sans-serif;
background:linear-gradient(180deg,#f7fbf9 0%,#f1f6f4 100%);
color:var(--ink); min-height:100vh; overflow-y:auto;
}
button,input{font:inherit}
button{cursor:pointer}
.app{width:min(1500px,98vw); margin:0 auto; padding:8px 10px 24px}
.topbar{
display:grid; grid-template-columns:1fr auto auto; gap:10px; align-items:center;
background:rgba(255,255,255,.94); border:2px solid var(--line); border-radius:18px;
box-shadow:var(--shadow); padding:8px 12px; position:sticky; top:4px; z-index:30;
}
.title h1{margin:0; font-size:clamp(24px,2.2vw,40px); color:#2e6f55; line-height:1}
.title .rule{margin-top:4px; font-size:clamp(13px,1.08vw,18px); color:var(--muted); font-weight:800}
.stats{display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end}
.stat{
background:#f7faf8; border:2px solid var(--line); border-radius:13px; padding:5px 10px;
min-width:92px; text-align:center; font-weight:900; font-size:clamp(13px,1vw,17px)
}
.stat strong{display:block; font-size:clamp(21px,1.8vw,31px)}
.timer{color:#b84a4a; background:#fff4f3; border-color:#f0c8c6}
.toolbar{display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end}
.btn{
border:none; border-radius:12px; padding:8px 12px; font-weight:900;
background:#edf2ef; color:#24342c; box-shadow:0 3px 9px rgba(0,0,0,.05);
}
.btn.primary{background:var(--green); color:white}
.btn.blue{background:var(--blue); color:white}
.btn.warn{background:#f1b15d; color:#412d14}
.btn.red{background:#d96b6b; color:white}
.btn:active{transform:translateY(1px)}
.modebar{
display:flex; gap:8px; align-items:center; justify-content:center; flex-wrap:wrap;
padding:8px 0 6px;
}
.modebtn{
border:2px solid var(--line); background:white; color:#34473d; border-radius:999px;
padding:7px 16px; font-weight:900; font-size:clamp(14px,1.05vw,18px)
}
.modebtn.active{background:#2f7157;color:white;border-color:#2f7157}
.board{display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:2px}
.zone{
min-height:220px; border-radius:18px; padding:8px; border:3px dashed;
display:flex; flex-direction:column; background:white;
}
.zone.zuo{border-color:#86b99e;background:#f5fbf7}
.zone.qi{border-color:#8eb8d9;background:#f6fbff}
.zone.dragover{transform:scale(1.008); box-shadow:0 0 0 5px rgba(82,145,111,.12)}
.zonehead{
display:flex; align-items:baseline; justify-content:center; gap:8px; margin-bottom:5px;
font-weight:900
}
.zonehead .han{font-size:clamp(34px,3.2vw,52px)}
.zonehead .py{font-size:clamp(17px,1.5vw,24px); color:var(--orange)}
.zonehead .kr{font-size:clamp(14px,1.15vw,19px); color:var(--muted)}
.zuo .han{color:#34765a}.qi .han{color:#3f77a5}
.matched{
display:flex; flex-wrap:wrap; gap:7px; justify-content:center; align-content:flex-start;
min-height:150px
}
.chip{
display:flex; align-items:center; gap:7px; background:white; border:2px solid var(--line);
border-radius:14px; padding:6px 9px; box-shadow:0 3px 9px rgba(0,0,0,.05);
min-width:150px; justify-content:center; animation:pop .22s ease-out
}
.chip.zuo{border-color:#9cc8b1;background:#edf7f1}
.chip.qi{border-color:#9dc2df;background:#edf6fc}
.chip .icon{font-size:28px}
.chip .text{line-height:1.05;text-align:left}
.chip .py{font-size:14px;color:#d46e38;font-weight:900}
.chip .zh{font-size:24px;font-weight:1000;white-space:nowrap;display:flex;align-items:center;gap:2px}
.chip.zuo .verb{color:#2e7155;}
.chip.qi .verb{color:#376f9a;}
.chip .noun{color:#1f2924;}
.chip .kr{font-size:13px;color:#59665f;font-weight:800}
.source{
margin-top:10px; background:white; border:2px solid var(--line); border-radius:18px;
box-shadow:var(--shadow); padding:8px 10px;
}
.sourcehead{
display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px;
font-weight:900; color:#3d4d45; font-size:clamp(14px,1.1vw,18px)
}
.cards{
display:grid; grid-template-columns:repeat(3, 1fr); gap:10px;
}
.card{
border:2px solid #b9ccd2; border-radius:15px; background:#fbfdfc; padding:10px 12px;
min-height:78px; display:flex; align-items:center; justify-content:center; gap:10px;
box-shadow:0 3px 8px rgba(0,0,0,.05); cursor:grab; transition:.15s; font-weight:1000;
}
.card:hover{transform:translateY(-2px)}
.card.selected{outline:4px solid #f2b35f; border-color:#d98a22}
.card.disabled{opacity:.42; pointer-events:none}
.card .icon{font-size:36px}
.card .txt{line-height:1.1;text-align:left}
.card .py{font-size:15px;color:#d46e38;font-weight:1000}
.card .zh{font-size:26px;font-weight:1000;white-space:nowrap}
.card .kr{font-size:14px;color:#5d6963;font-weight:1000}
.relay{
margin-top:10px; display:none; background:white; border:2px solid var(--line); border-radius:18px;
box-shadow:var(--shadow); padding:9px 12px; text-align:center
}
.relay.active{display:block}
.relay-label{font-weight:900;color:#5d6a63;font-size:15px}
.relay-card{
margin:5px auto 7px; width:min(520px,96%); min-height:112px; border:3px solid #b8d5c5;
background:#f7fcf9; border-radius:20px; display:flex; align-items:center; justify-content:center; gap:18px;
}
.relay-card .icon{font-size:52px}
.relay-card .py{font-size:20px;color:#d46e38;font-weight:1000}
.relay-card .zh{font-size:40px;font-weight:1000}
.relay-card .kr{font-size:17px;color:#627069;font-weight:1000}
.relay-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:min(720px,96%);margin:0 auto}
.answerbtn{
border:none;border-radius:16px;padding:12px 8px;font-weight:1000;font-size:clamp(24px,2.1vw,36px)
}
.answerbtn.zuo{background:#dff1e7;color:#2e7155;border:3px solid #88b99e}
.answerbtn.qi{background:#e4f1fa;color:#376f9a;border:3px solid #8fb8d8}
.feedback{
min-height:42px;display:flex;align-items:center;justify-content:center;
font-size:clamp(22px,2vw,32px);font-weight:1000;margin-top:3px
}
.feedback.good{color:#2f7a58}.feedback.bad{color:#c44f4f}
.footerbar{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin-top:10px}
.hidden{display:none!important}
.countdown{
position:fixed;inset:0;background:rgba(18,35,27,.76);display:none;align-items:center;justify-content:center;
z-index:1000;color:white;font-weight:1000;font-size:min(26vw,260px);text-shadow:0 10px 30px rgba(0,0,0,.35)
}
.countdown.show{display:flex}
.finishbar{
display:none;margin-top:10px;background:#fff8e8;border:2px solid #ecd29b;border-radius:16px;
padding:10px 14px;text-align:center;font-weight:900;font-size:clamp(17px,1.3vw,22px)
}
.finishbar.show{display:block}
.finishbar strong{color:#a34b3f}
.editor{
position:fixed;right:10px;top:80px;width:min(540px,94vw);max-height:82vh;overflow:auto;
z-index:200;background:white;border:2px solid var(--line);border-radius:18px;box-shadow:0 16px 45px rgba(0,0,0,.18);
padding:12px;display:none
}
.editor.show{display:block}
.editor h2{margin:0 0 4px;font-size:24px}
.editor p{margin:0 0 9px;color:#66736c;font-weight:700;font-size:14px}
.editgrid{display:grid;grid-template-columns:44px 1fr 1fr 1fr 72px;gap:5px;align-items:center}
.editgrid input{
width:100%;border:1.5px solid #cfdcd5;border-radius:9px;padding:6px 7px;font-weight:800
}
.editgrid .head{font-size:12px;color:#66736c;text-align:center;font-weight:900}
.editor-actions{display:flex;gap:7px;justify-content:flex-end;margin-top:10px;flex-wrap:wrap}
.note{font-size:12px;color:#748078;margin-top:7px}
.hide-pinyin .py{display:none!important}
.hide-korean .kr{display:none!important}
.result-overlay{
position:fixed; inset:0; background:rgba(18,35,27,.82); backdrop-filter:blur(6px);
z-index:1200; display:none; align-items:center; justify-content:center; padding:15px;
}
.result-overlay.show{display:flex}
.result-inner{
background:linear-gradient(180deg,#ffffff 0%,#f4f9f6 100%);
border-radius:24px; padding:24px; width:min(860px,96vw); max-height:90vh; overflow-y:auto;
box-shadow:0 20px 50px rgba(0,0,0,.3); text-align:center;
}
.result-title{
font-size:clamp(28px,3.5vw,48px); font-weight:1000; color:#2f7157; margin-bottom:4px;
}
.result-label{
font-size:clamp(16px,1.5vw,22px); font-weight:900; color:#5b6962; margin-bottom:2px;
}
.result-time{
font-size:clamp(48px,8vw,90px); line-height:1; font-weight:1000; color:#b84a4a;
letter-spacing:.02em; margin-bottom:15px;
}
.result-review{
background:white; border:2px solid var(--line); border-radius:18px; padding:12px 14px; margin-bottom:18px;
}
.result-review-title{
font-size:16px; font-weight:1000; color:#3d4d45; margin-bottom:10px; text-align:left;
}
.result-grid{
display:grid; grid-template-columns:1fr 1fr; gap:10px;
}
.result-col{
border-radius:14px; padding:8px; display:flex; flex-direction:column; gap:6px; min-height:100px;
}
.result-col.zuo{background:#f0f7f3; border:1px solid #a3d0b8}
.result-col.qi{background:#f0f6fa; border:1px solid #a4c7e5}
.result-col-head{font-weight:1000; font-size:18px; margin-bottom:4px}
.result-col.zuo .result-col-head{color:#2e7155}
.result-col.qi .result-col-head{color:#376f9a}
.result-close{
font-size:clamp(16px,1.4vw,22px); padding:10px 28px; width:100%; max-width:320px;
}
@keyframes pop{0%{transform:scale(.85);opacity:.4}100%{transform:scale(1);opacity:1}}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-9px)}75%{transform:translateX(9px)}}
@media(max-width:850px){
.cards{grid-template-columns:repeat(2, 1fr)}
.result-grid{grid-template-columns:1fr}
}
@media(max-width:760px){
.board{grid-template-columns:1fr}
.topbar{position:static;grid-template-columns:1fr}.stats,.toolbar{justify-content:center}
.chip{min-width:135px}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="title">
<h1>🚦 交通工具接力|교통수단 릴레이</h1>
<div class="rule">拖到“坐 / 骑”|“坐 / 骑”에 넣기 ❌ 错误 +10秒|오답 +10초</div>
</div>
<div class="stats">
<div class="stat timer">⏱ 计时|시간<strong id="timer">00:00</strong></div> </div>
<div class="toolbar">
<button class="btn primary" id="startBtn">▶ 开始|시작</button> <button class="btn warn" id="editBtn">✏ 编辑|편집</button>
<button class="btn red" id="resetBtn">↻ 重置|초기화</button>
</div>
</header>
<div class="modebar">
<button class="modebtn active" data-mode="practice">练习|연습</button>
<button class="modebtn" data-mode="relay">接力赛|릴레이</button>
</div>
<section class="board">
<div class="zone zuo" data-type="坐">
<div class="zonehead"><span class="han">坐</span><span class="py">zuò</span><span class="kr">앉아서 타다</span></div>
<div class="matched" id="zuoList"></div>
</div>
<div class="zone qi" data-type="骑">
<div class="zonehead"><span class="han">骑</span><span class="py">qí</span><span class="kr">걸쳐서 타다</span></div>
<div class="matched" id="qiList"></div>
</div>
</section>
<section class="source" id="practiceArea">
<div class="sourcehead">
<span>选择或拖拽|선택 또는 드래그</span>
<span id="remainText">剩余 6|남음 6</span>
</div>
<div class="cards" id="cards"></div>
</section>
<section class="relay" id="relayArea">
<div class="relay-label">当前题目|현재 문제</div>
<div class="relay-card" id="relayCard"></div>
<div class="relay-actions">
<button class="answerbtn zuo" data-answer="坐">坐 <span class="py">zuò</span><br><span class="kr">앉아서 타다</span></button>
<button class="answerbtn qi" data-answer="骑">骑 <span class="py">qí</span><br><span class="kr">걸쳐서 타다</span></button>
</div>
<div class="feedback" id="feedback"></div>
</section>
<div class="finishbar" id="finishbar">
<button class="btn primary" id="readBtn">🔊 一起读|같이 읽기</button>
</div>
<div class="footerbar">
<button class="btn" id="pinyinBtn">拼音:开|병음: 켜짐</button>
<button class="btn" id="koreanBtn">韩语:开|한국어: 켜짐</button>
</div>
</div>
<div class="result-overlay" id="resultOverlay">
<div class="result-inner">
<div class="result-title">🎉 完成|완료</div>
<div class="result-label">最终用时|최종 시간</div>
<div class="result-time" id="resultTime">00:00</div>
<div class="result-review">
<div class="result-review-title">📌 搭配成果巩固|짝맞추기 결과</div>
<div class="result-grid">
<div class="result-col zuo">
<div class="result-col-head">坐 zuò</div>
<div id="resultZuoList"></div>
</div>
<div class="result-col qi">
<div class="result-col-head">骑 qí</div>
<div id="resultQiList"></div>
</div>
</div>
</div>
<button class="btn primary result-close" id="resultClose">确定|확인</button>
</div>
</div>
<div class="countdown" id="countdown"></div>
<div class="editor" id="editor">
<h2>编辑词语|단어 편집</h2>
<p>可修改图标、中文、拼音、韩语和“坐/骑”。修改后点击“应用”。</p>
<div class="editgrid" id="editGrid"></div>
<div class="editor-actions">
<button class="btn" id="restoreBtn">恢复默认|기본값</button>
<button class="btn" id="closeEditBtn">关闭|닫기</button>
<button class="btn primary" id="applyEditBtn">应用|적용</button>
</div>
<div class="note">内容会保存在当前浏览器|내용은 현재 브라우저에 저장됩니다.</div>
</div>
<script>
const DEFAULT_VOCAB = [
{icon:'✈️',zh:'飞机',py:'fēijī',kr:'비행기',type:'坐'},
{icon:'🚆',zh:'火车',py:'huǒchē',kr:'기차',type:'坐'},
{icon:'🚇',zh:'地铁',py:'dìtiě',kr:'지하철',type:'坐'},
{icon:'🚢',zh:'船',py:'chuán',kr:'배',type:'坐'},
{icon:'🚌',zh:'公交车',py:'gōngjiāochē',kr:'버스',type:'坐'},
{icon:'🚕',zh:'出租车',py:'chūzūchē',kr:'택시',type:'坐'},
{icon:'🚗',zh:'汽车',py:'qìchē',kr:'자동차',type:'坐'},
{icon:'🏍️',zh:'摩托车',py:'mótuōchē',kr:'오토바이',type:'骑'},
{icon:'🚲',zh:'自行车',py:'zìxíngchē',kr:'자전거',type:'骑'},
{icon:'🐎',zh:'马',py:'mǎ',kr:'말',type:'骑'}
];
let vocab = JSON.parse(localStorage.getItem('transportRelayVocab')||'null') || structuredClone(DEFAULT_VOCAB);
let activeVocab = [];
let mode='practice', started=false, sec=0, timer=null, ok=0, bad=0, selected=null;
let pending=[], relayIndex=0, readingTimer=null;
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
function esc(s){return String(s).replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
function timeText(){
return String(Math.floor(sec/60)).padStart(2,'0')+':'+String(sec%60).padStart(2,'0');
}
function updateStats(){
$('#timer').textContent=timeText();
$('#remainText').textContent=`剩余 ${Math.max(activeVocab.length-ok,0)}|남음 ${Math.max(activeVocab.length-ok,0)}`;
}
function beep(kind){
try{
const ctx=new (window.AudioContext||window.webkitAudioContext)();
const o=ctx.createOscillator(),g=ctx.createGain();o.connect(g);g.connect(ctx.destination);
o.frequency.value=kind==='ok'?720:kind==='bad'?170:520; o.type=kind==='bad'?'sawtooth':'sine';
g.gain.setValueAtTime(.13,ctx.currentTime);g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.18);
o.start();o.stop(ctx.currentTime+.19);
}catch(e){}
}
function startTimer(){clearInterval(timer);timer=setInterval(()=>{sec++;updateStats()},1000)}
// 修改:确保每次抽取的 6 个词中至少有 2 个“骑”
function pickActiveVocab(){
const qiList = shuffle(vocab.filter(v => v.type === '骑'));
const zuoList = shuffle(vocab.filter(v => v.type === '坐'));
// 强制取至少2个“骑”
const selectedQi = qiList.slice(0, Math.max(2, Math.min(qiList.length, 2)));
// 剩余需要从其它词语中补齐
const remainingPool = shuffle([...qiList.slice(selectedQi.length), ...zuoList]);
const needCount = 6 - selectedQi.length;
const selectedOthers = remainingPool.slice(0, needCount);
// 打乱后返回6个词
return shuffle([...selectedQi, ...selectedOthers]);
}
function resetGame(){
clearInterval(timer);clearInterval(readingTimer); started=false;sec=0;ok=0;bad=0;selected=null;
$('#startBtn').disabled=false; $('#startBtn').textContent='▶ 开始|시작';
$('#zuoList').innerHTML=''; $('#qiList').innerHTML=''; $('#finishbar').classList.remove('show');
$('#resultOverlay').classList.remove('show');
$('#feedback').textContent=''; $('#feedback').className='feedback';
activeVocab = pickActiveVocab();
pending = activeVocab.map((v,i)=>({...v,id:i}));
relayIndex=0;
renderPractice();renderRelay();updateStats();
}
function cardHTML(v){return `<span class="icon">${esc(v.icon)}</span><div class="txt"><div class="py">${esc(v.py)}</div><div class="zh">${esc(v.zh)}</div><div class="kr">${esc(v.kr)}</div></div>`}
function renderPractice(){
$('#cards').innerHTML='';
pending.forEach(v=>{
const el=document.createElement('div');el.className='card'+(started?'':' disabled');el.draggable=started;el.dataset.id=v.id;
el.innerHTML=cardHTML(v);
el.addEventListener('click',()=>selectCard(el,v));
el.addEventListener('dragstart',e=>{if(!started)return;e.dataTransfer.setData('text/plain',String(v.id))});
$('#cards').appendChild(el);
});
}
function renderRelay(){
const v=pending[relayIndex];
if(!v){$('#relayCard').innerHTML='<div style="font-weight:900;font-size:28px">完成|완료</div>';return}
$('#relayCard').innerHTML=cardHTML(v);
}
function selectCard(el,v){
if(!started)return;
$$('.card').forEach(x=>x.classList.remove('selected'));
selected={el,v};el.classList.add('selected');
}
function addMatched(v){
const chip=document.createElement('div');chip.className='chip '+(v.type==='坐'?'zuo':'qi');
chip.dataset.zh=v.zh;
chip.innerHTML=`<span class="icon">${esc(v.icon)}</span><div class="text"><div class="py">${v.type==='坐'?'zuò':'qí'} ${esc(v.py)}</div><div class="zh"><span class="verb">${v.type}</span><span class="noun">${esc(v.zh)}</span></div><div class="kr">${esc(v.kr)}</div></div>`;
(v.type==='坐'?$('#zuoList'):$('#qiList')).appendChild(chip);
}
function correct(v){
beep('ok');ok++;addMatched(v);
pending=pending.filter(x=>x.id!==v.id);
updateStats();
if(ok===activeVocab.length){finish();return true}
return false;
}
function wrong(el){
beep('bad');bad++;sec+=10;updateStats();
if(el){el.style.animation='shake .32s';setTimeout(()=>el.style.animation='',330)}
}
function handlePracticeAnswer(v,zoneType,el){
if(v.type===zoneType){
correct(v); renderPractice(); selected=null;
}else wrong(el);
}
$$('.zone').forEach(z=>{
z.addEventListener('dragover',e=>{e.preventDefault();if(started&&mode==='practice')z.classList.add('dragover')});
z.addEventListener('dragleave',()=>z.classList.remove('dragover'));
z.addEventListener('drop',e=>{
e.preventDefault();z.classList.remove('dragover');if(!started||mode!=='practice')return;
const id=Number(e.dataTransfer.getData('text/plain'));const v=pending.find(x=>x.id===id);
const el=[...$('#cards').children].find(x=>Number(x.dataset.id)===id); if(v)handlePracticeAnswer(v,z.dataset.type,el);
});
z.addEventListener('click',()=>{
if(started&&mode==='practice'&&selected)handlePracticeAnswer(selected.v,z.dataset.type,selected.el)
});
});
$$('.answerbtn').forEach(b=>b.addEventListener('click',()=>{
if(!started||mode!=='relay')return;
const v=pending[relayIndex]; if(!v)return;
if(v.type===b.dataset.answer){
$('#feedback').textContent=`✅ ${v.type}${v.zh}`;$('#feedback').className='feedback good';
const done=correct(v);
if(!done){relayIndex=0;setTimeout(()=>{$('#feedback').textContent='';renderRelay()},420)}
}else{
wrong(b);$('#feedback').textContent='❌ 再试一次|다시!';$('#feedback').className='feedback bad';
}
}));
function finish(){
clearInterval(timer);started=false;$('#startBtn').disabled=false;$('#startBtn').textContent='▶ 再玩|다시';
$('#resultTime').textContent=timeText();
const zuoMatched = activeVocab.filter(v=>v.type==='坐');
const qiMatched = activeVocab.filter(v=>v.type==='骑');
const createChipHTML = v => `
<div class="chip ${v.type==='坐'?'zuo':'qi'}" style="margin:2px 0;">
<span class="icon">${esc(v.icon)}</span>
<div class="text">
<div class="py">${v.type==='坐'?'zuò':'qí'} ${esc(v.py)}</div>
<div class="zh"><span class="verb">${v.type}</span><span class="noun">${esc(v.zh)}</span></div>
<div class="kr">${esc(v.kr)}</div>
</div>
</div>`;
$('#resultZuoList').innerHTML = zuoMatched.map(createChipHTML).join('') || '<div style="color:#888;font-size:12px;">无</div>';
$('#resultQiList').innerHTML = qiMatched.map(createChipHTML).join('') || '<div style="color:#888;font-size:12px;">无</div>';
$('#resultOverlay').classList.add('show');
$('#feedback').textContent='🎉 完成|완료';$('#feedback').className='feedback good';
renderPractice();renderRelay();
}
async function countdownStart(){
if(started)return;
const c=$('#countdown');c.classList.add('show');
for(const t of ['3','2','1','GO!']){c.textContent=t;beep('go');await new Promise(r=>setTimeout(r,t==='GO!'?450:650))}
c.classList.remove('show');started=true;$('#startBtn').disabled=true;startTimer();renderPractice();
}
$('#startBtn').addEventListener('click',()=>{if(ok===activeVocab.length)resetGame();countdownStart()});
$('#resetBtn').addEventListener('click',resetGame);
$('#resultClose').addEventListener('click',()=>{ $('#resultOverlay').classList.remove('show'); $('#finishbar').classList.add('show'); window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); });
$$('.modebtn').forEach(b=>b.addEventListener('click',()=>{
if(started)return alert('请先重置|먼저 초기화하세요');
mode=b.dataset.mode;$$('.modebtn').forEach(x=>x.classList.toggle('active',x===b));
$('#practiceArea').classList.toggle('hidden',mode!=='practice');$('#relayArea').classList.toggle('active',mode==='relay');
resetGame();
}));
$('#pinyinBtn').addEventListener('click',()=>{
const on=document.body.classList.toggle('hide-pinyin');
$('#pinyinBtn').textContent=on?'拼音:关|병음: 꺼짐':'拼音:开|병음: 켜짐';
});
$('#koreanBtn').addEventListener('click',()=>{
const on=document.body.classList.toggle('hide-korean');
$('#koreanBtn').textContent=on?'韩语:关|한국어: 꺼짐':'韩语:开|한국어: 켜짐';
});
$('#readBtn').addEventListener('click',()=>{
clearInterval(readingTimer);
const chips=$$('.chip');let i=0;
chips.forEach(c=>{c.style.outline='';c.style.transform=''});
if(!chips.length)return;
const step=()=>{
chips.forEach(c=>{c.style.outline='';c.style.transform=''});
if(i>=chips.length){clearInterval(readingTimer);return}
chips[i].style.outline='5px solid #f2b35f';chips[i].style.transform='scale(1.06)';
chips[i].scrollIntoView({behavior:'smooth',block:'center'});i++;
};
step();readingTimer=setInterval(step,1200);
});
/* editor */
function renderEditor(){
const g=$('#editGrid');
g.innerHTML='<div class="head">图标</div><div class="head">中文</div><div class="head">拼音</div><div class="head">韩语</div><div class="head">坐/骑</div>';
vocab.forEach((v,i)=>{
[['icon',v.icon],['zh',v.zh],['py',v.py],['kr',v.kr],['type',v.type]].forEach(([k,val])=>{
const input=document.createElement('input');input.value=val;input.dataset.i=i;input.dataset.k=k;g.appendChild(input);
});
});
}
$('#editBtn').addEventListener('click',()=>{if(started)return alert('请先重置|먼저 초기화하세요');renderEditor();$('#editor').classList.add('show')});
$('#closeEditBtn').addEventListener('click',()=>$('#editor').classList.remove('show'));
$('#applyEditBtn').addEventListener('click',()=>{
const next=structuredClone(vocab);
$$('#editGrid input').forEach(inp=>next[Number(inp.dataset.i)][inp.dataset.k]=inp.value.trim());
if(next.some(v=>!v.zh||!v.py||!v.kr||!['坐','骑'].includes(v.type))){
return alert('请检查内容。“坐/骑”必须填写:坐 或 骑。|내용을 확인하세요.');
}
vocab=next;localStorage.setItem('transportRelayVocab',JSON.stringify(vocab));$('#editor').classList.remove('show');resetGame();
});
$('#restoreBtn').addEventListener('click',()=>{
vocab=structuredClone(DEFAULT_VOCAB);localStorage.removeItem('transportRelayVocab');renderEditor();resetGame();
});
resetGame();
</script>
</body>
</html>