829 lines
24 KiB
JavaScript
829 lines
24 KiB
JavaScript
const DAILY_GOAL = 5,
|
||
ROUND_SIZE = 5;
|
||
|
||
// === AUDIO with mobile unlock ===
|
||
var AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||
var audioCtx = null,
|
||
audioUnlocked = false;
|
||
function unlockAudio() {
|
||
if (audioUnlocked || !audioCtx) return;
|
||
var buf = audioCtx.createBuffer(1, 1, 22050);
|
||
var src = audioCtx.createBufferSource();
|
||
src.buffer = buf;
|
||
src.connect(audioCtx.destination);
|
||
src.start(0);
|
||
if (audioCtx.state === "suspended") audioCtx.resume();
|
||
audioUnlocked = true;
|
||
}
|
||
function ensureAudio() {
|
||
if (!audioCtx) audioCtx = new AudioCtx();
|
||
unlockAudio();
|
||
}
|
||
document.addEventListener(
|
||
"touchstart",
|
||
function () {
|
||
ensureAudio();
|
||
},
|
||
{ once: true },
|
||
);
|
||
document.addEventListener(
|
||
"click",
|
||
function () {
|
||
ensureAudio();
|
||
},
|
||
{ once: true },
|
||
);
|
||
function playTone(f, d, t, v) {
|
||
if (!state.sound) return;
|
||
t = t || "sine";
|
||
v = v || 0.3;
|
||
try {
|
||
ensureAudio();
|
||
var o = audioCtx.createOscillator(),
|
||
g = audioCtx.createGain();
|
||
o.type = t;
|
||
o.frequency.value = f;
|
||
g.gain.setValueAtTime(v, audioCtx.currentTime);
|
||
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + d);
|
||
o.connect(g);
|
||
g.connect(audioCtx.destination);
|
||
o.start();
|
||
o.stop(audioCtx.currentTime + d);
|
||
} catch (e) {}
|
||
}
|
||
function sfxCorrect() {
|
||
playTone(523, 0.12);
|
||
setTimeout(function () {
|
||
playTone(659, 0.12);
|
||
}, 80);
|
||
setTimeout(function () {
|
||
playTone(784, 0.2);
|
||
}, 160);
|
||
}
|
||
function sfxWrong() {
|
||
playTone(200, 0.15, "square", 0.2);
|
||
setTimeout(function () {
|
||
playTone(160, 0.2, "square", 0.2);
|
||
}, 120);
|
||
}
|
||
function sfxClick() {
|
||
playTone(440, 0.06, "sine", 0.15);
|
||
}
|
||
function sfxCombo() {
|
||
playTone(523, 0.1);
|
||
setTimeout(function () {
|
||
playTone(659, 0.1);
|
||
}, 60);
|
||
setTimeout(function () {
|
||
playTone(784, 0.1);
|
||
}, 120);
|
||
setTimeout(function () {
|
||
playTone(1047, 0.25);
|
||
}, 180);
|
||
}
|
||
function sfxComplete() {
|
||
[523, 587, 659, 784, 1047].forEach(function (n, i) {
|
||
setTimeout(function () {
|
||
playTone(n, 0.2, "sine", 0.25);
|
||
}, i * 100);
|
||
});
|
||
}
|
||
|
||
// === STATE ===
|
||
const APP_VERSION = 2;
|
||
var state = {
|
||
sound: true,
|
||
bigFont: false,
|
||
difficulty: 1,
|
||
score: 0,
|
||
combo: 0,
|
||
maxCombo: 0,
|
||
todayCount: 0,
|
||
totalCount: 0,
|
||
streak: 0,
|
||
checkinDays: [],
|
||
round: 0,
|
||
roundResults: [],
|
||
currentIdiom: null,
|
||
blankIndices: [],
|
||
selectedBlank: -1,
|
||
filledChars: {},
|
||
hintUsed: false,
|
||
answered: false,
|
||
usedIdiomIndices: {},
|
||
wrongBook: [],
|
||
reviewMode: false,
|
||
};
|
||
var currentOptions = [];
|
||
function getToday() {
|
||
return new Date().toISOString().slice(0, 10);
|
||
}
|
||
function saveState() {
|
||
localStorage.setItem(
|
||
"chengyu_s",
|
||
JSON.stringify({
|
||
sound: state.sound,
|
||
bigFont: state.bigFont,
|
||
difficulty: state.difficulty,
|
||
totalCount: state.totalCount,
|
||
streak: state.streak,
|
||
checkinDays: state.checkinDays,
|
||
maxCombo: state.maxCombo,
|
||
}),
|
||
);
|
||
}
|
||
function saveWrongBook() {
|
||
localStorage.setItem("chengyu_wb", JSON.stringify(state.wrongBook));
|
||
}
|
||
function loadWrongBook() {
|
||
try {
|
||
state.wrongBook = JSON.parse(localStorage.getItem("chengyu_wb") || "[]");
|
||
} catch (e) {
|
||
state.wrongBook = [];
|
||
}
|
||
}
|
||
function addToWrongBook(idiom) {
|
||
var ex = null;
|
||
for (var i = 0; i < state.wrongBook.length; i++) {
|
||
if (state.wrongBook[i].w === idiom.w) {
|
||
ex = state.wrongBook[i];
|
||
break;
|
||
}
|
||
}
|
||
if (ex) {
|
||
ex.count++;
|
||
ex.last = getToday();
|
||
} else {
|
||
state.wrongBook.push({
|
||
w: idiom.w,
|
||
p: idiom.p,
|
||
e: idiom.e,
|
||
l: idiom.l,
|
||
count: 1,
|
||
last: getToday(),
|
||
});
|
||
}
|
||
saveWrongBook();
|
||
renderWbBadge();
|
||
}
|
||
function removeFromWrongBook(w) {
|
||
state.wrongBook = state.wrongBook.filter(function (x) {
|
||
return x.w !== w;
|
||
});
|
||
saveWrongBook();
|
||
renderWbBadge();
|
||
}
|
||
function renderWbBadge() {
|
||
var b = document.getElementById("wbBadge");
|
||
if (!b) return;
|
||
b.textContent = state.wrongBook.length;
|
||
b.className = "wb-badge" + (state.wrongBook.length === 0 ? " empty" : "");
|
||
}
|
||
function showWrongBookModal() {
|
||
document.getElementById("wrongBookModal").classList.add("show");
|
||
document.getElementById("wbCount").textContent = state.wrongBook.length;
|
||
var list = document.getElementById("wbList");
|
||
var startBtn = document.getElementById("btnStartReview");
|
||
if (!state.wrongBook.length) {
|
||
list.innerHTML = '<div class="wb-empty">🎉 暂无错题,梅子很棒!</div>';
|
||
startBtn.style.display = "none";
|
||
} else {
|
||
startBtn.style.display = "";
|
||
list.innerHTML = "";
|
||
var sorted = state.wrongBook.slice().sort(function (a, b) {
|
||
return b.count - a.count;
|
||
});
|
||
sorted.forEach(function (item) {
|
||
var d = document.createElement("div");
|
||
d.className = "wb-item";
|
||
d.innerHTML =
|
||
'<div class="wb-word">' +
|
||
item.w +
|
||
'</div><div class="wb-pinyin">' +
|
||
item.p +
|
||
'</div><div class="wb-explain">' +
|
||
item.e +
|
||
'</div><div class="wb-meta">错误 ' +
|
||
item.count +
|
||
" 次</div>";
|
||
list.appendChild(d);
|
||
});
|
||
}
|
||
}
|
||
function startReview() {
|
||
document.getElementById("wrongBookModal").classList.remove("show");
|
||
state.reviewMode = true;
|
||
state.usedIdiomIndices = {};
|
||
startRound();
|
||
}
|
||
function checkWhatsNew() {
|
||
var v = parseInt(localStorage.getItem("chengyu_v") || "0");
|
||
if (v < APP_VERSION) {
|
||
document.getElementById("whatsNewModal").classList.add("show");
|
||
}
|
||
}
|
||
function saveTodayState() {
|
||
localStorage.setItem(
|
||
"chengyu_t",
|
||
JSON.stringify({
|
||
d: getToday(),
|
||
s: state.score,
|
||
c: state.todayCount,
|
||
cb: state.combo,
|
||
}),
|
||
);
|
||
}
|
||
function loadState() {
|
||
try {
|
||
var s = JSON.parse(localStorage.getItem("chengyu_s") || "{}");
|
||
state.sound = s.sound !== undefined ? s.sound : true;
|
||
state.bigFont = s.bigFont || false;
|
||
state.totalCount = s.totalCount || 0;
|
||
state.streak = s.streak || 0;
|
||
state.checkinDays = s.checkinDays || [];
|
||
state.maxCombo = s.maxCombo || 0;
|
||
state.difficulty = s.difficulty || 1;
|
||
var t = JSON.parse(localStorage.getItem("chengyu_t") || "{}");
|
||
if (t.d === getToday()) {
|
||
state.score = t.s || 0;
|
||
state.todayCount = t.c || 0;
|
||
state.combo = t.cb || 0;
|
||
}
|
||
updateStreak();
|
||
} catch (e) {}
|
||
}
|
||
function updateStreak() {
|
||
var today = getToday(),
|
||
days = state.checkinDays;
|
||
if (!days.length) {
|
||
state.streak = 0;
|
||
return;
|
||
}
|
||
var sorted = days.slice().sort().reverse();
|
||
var streak = 0,
|
||
cd = new Date(today + "T00:00:00");
|
||
if (sorted[0] === today) {
|
||
for (var i = 0; i < sorted.length; i++) {
|
||
var e = new Date(cd);
|
||
e.setDate(e.getDate() - i);
|
||
if (sorted[i] === e.toISOString().slice(0, 10)) streak++;
|
||
else break;
|
||
}
|
||
} else {
|
||
var y = new Date(cd);
|
||
y.setDate(y.getDate() - 1);
|
||
var ys = y.toISOString().slice(0, 10);
|
||
if (sorted[0] === ys) {
|
||
for (var i = 0; i < sorted.length; i++) {
|
||
var e = new Date(y);
|
||
e.setDate(e.getDate() - i);
|
||
if (sorted[i] === e.toISOString().slice(0, 10)) streak++;
|
||
else break;
|
||
}
|
||
}
|
||
}
|
||
state.streak = streak;
|
||
}
|
||
function tryCheckin() {
|
||
var t = getToday();
|
||
if (state.todayCount >= DAILY_GOAL && state.checkinDays.indexOf(t) === -1) {
|
||
state.checkinDays.push(t);
|
||
if (state.checkinDays.length > 60)
|
||
state.checkinDays = state.checkinDays.slice(-60);
|
||
updateStreak();
|
||
saveState();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// === GAME LOGIC ===
|
||
function pickIdiom() {
|
||
if (state.reviewMode && state.wrongBook.length) {
|
||
var pool = state.wrongBook.filter(function (x) {
|
||
return !state.usedIdiomIndices[x.w];
|
||
});
|
||
if (!pool.length) {
|
||
state.usedIdiomIndices = {};
|
||
pool = state.wrongBook.slice();
|
||
}
|
||
var item = pool[Math.floor(Math.random() * pool.length)];
|
||
state.usedIdiomIndices[item.w] = 1;
|
||
return item;
|
||
}
|
||
var pool = IDIOMS.filter(function (i) {
|
||
return i.l <= state.difficulty;
|
||
});
|
||
var a = 0,
|
||
idx;
|
||
do {
|
||
idx = Math.floor(Math.random() * pool.length);
|
||
a++;
|
||
} while (state.usedIdiomIndices[pool[idx].w] && a < 100);
|
||
state.usedIdiomIndices[pool[idx].w] = 1;
|
||
return pool[idx];
|
||
}
|
||
function generateBlanks(w, d) {
|
||
var n = d === 1 ? 1 : d === 2 ? 2 : 3;
|
||
var idx = [0, 1, 2, 3];
|
||
for (var i = 3; i > 0; i--) {
|
||
var j = Math.floor(Math.random() * (i + 1));
|
||
var tmp = idx[i];
|
||
idx[i] = idx[j];
|
||
idx[j] = tmp;
|
||
}
|
||
return idx.slice(0, n).sort();
|
||
}
|
||
function generateOptions(idiom, blanks) {
|
||
var correct = blanks.map(function (i) {
|
||
return idiom.w[i];
|
||
});
|
||
var decoyN = blanks.length <= 1 ? 3 : blanks.length === 2 ? 4 : 5;
|
||
var allC = {};
|
||
IDIOMS.forEach(function (it) {
|
||
for (var i = 0; i < it.w.length; i++) allC[it.w[i]] = 1;
|
||
});
|
||
var pool = Object.keys(allC).filter(function (c) {
|
||
return correct.indexOf(c) === -1;
|
||
});
|
||
var decoys = [],
|
||
used = {};
|
||
correct.forEach(function (c) {
|
||
used[c] = 1;
|
||
});
|
||
while (decoys.length < decoyN && pool.length) {
|
||
var i = Math.floor(Math.random() * pool.length);
|
||
if (!used[pool[i]]) {
|
||
decoys.push(pool[i]);
|
||
used[pool[i]] = 1;
|
||
}
|
||
pool.splice(i, 1);
|
||
}
|
||
var opts = correct.concat(decoys);
|
||
for (var i = opts.length - 1; i > 0; i--) {
|
||
var j = Math.floor(Math.random() * (i + 1));
|
||
var tmp = opts[i];
|
||
opts[i] = opts[j];
|
||
opts[j] = tmp;
|
||
}
|
||
return opts;
|
||
}
|
||
|
||
// === RENDER ===
|
||
function renderStats() {
|
||
document.getElementById("statScore").textContent = state.score;
|
||
document.getElementById("statCombo").textContent = state.combo;
|
||
document.getElementById("statTotal").textContent = state.totalCount;
|
||
document.getElementById("statStreak").textContent = state.streak;
|
||
}
|
||
function renderProgress() {
|
||
var bar = document.getElementById("progressBar");
|
||
bar.innerHTML = "";
|
||
for (var i = 0; i < ROUND_SIZE; i++) {
|
||
var d = document.createElement("div");
|
||
d.className =
|
||
"progress-dot" +
|
||
(i < state.round
|
||
? state.roundResults[i]
|
||
? " done"
|
||
: " skip"
|
||
: i === state.round
|
||
? " current"
|
||
: "");
|
||
bar.appendChild(d);
|
||
}
|
||
}
|
||
function renderIdiom() {
|
||
var el = document.getElementById("idiomDisplay");
|
||
el.innerHTML = "";
|
||
var idiom = state.currentIdiom,
|
||
pp = idiom.p.split(" ");
|
||
for (var i = 0; i < idiom.w.length; i++) {
|
||
var box = document.createElement("div");
|
||
box.className = "char-box";
|
||
if (state.blankIndices.indexOf(i) !== -1) {
|
||
box.classList.add("blank");
|
||
if (state.filledChars[i]) {
|
||
box.classList.remove("blank");
|
||
box.classList.add("filled");
|
||
box.textContent = state.filledChars[i];
|
||
} else {
|
||
box.textContent = "?";
|
||
if (state.selectedBlank === i) box.classList.add("selected");
|
||
}
|
||
(function (idx) {
|
||
box.addEventListener("click", function () {
|
||
selectBlank(idx);
|
||
});
|
||
})(i);
|
||
} else {
|
||
box.textContent = idiom.w[i];
|
||
var py = document.createElement("span");
|
||
py.className = "pinyin-hint";
|
||
py.textContent = pp[i] || "";
|
||
box.appendChild(py);
|
||
}
|
||
el.appendChild(box);
|
||
}
|
||
}
|
||
function renderOptions(opts) {
|
||
var grid = document.getElementById("optionsGrid");
|
||
grid.innerHTML = "";
|
||
var filled = Object.keys(state.filledChars).map(function (k) {
|
||
return state.filledChars[k];
|
||
});
|
||
opts.forEach(function (ch) {
|
||
var btn = document.createElement("button");
|
||
btn.className = "option-btn";
|
||
btn.textContent = ch;
|
||
if (filled.indexOf(ch) !== -1) btn.classList.add("used");
|
||
if (state.answered) btn.classList.add("disabled");
|
||
btn.addEventListener("click", function () {
|
||
selectOption(ch);
|
||
});
|
||
grid.appendChild(btn);
|
||
});
|
||
}
|
||
function renderCombo() {
|
||
var el = document.getElementById("comboDisplay");
|
||
var toast = document.getElementById("comboToast");
|
||
if (state.combo >= 3) {
|
||
var msgs = [
|
||
"梅子不错哦~",
|
||
"太棒了梅子!",
|
||
"梅子真厉害!",
|
||
"学富五车!",
|
||
"梅子无人能挡!",
|
||
"登峰造极!",
|
||
];
|
||
var i = Math.min(Math.floor((state.combo - 3) / 2), msgs.length - 1);
|
||
var txt = "🔥 连击 ×" + state.combo + " " + msgs[i];
|
||
el.innerHTML = '<span class="combo-text">' + txt + "</span>";
|
||
if (toast) {
|
||
toast.textContent = txt;
|
||
toast.classList.add("show");
|
||
clearTimeout(window._comboTimer);
|
||
window._comboTimer = setTimeout(function () {
|
||
toast.classList.remove("show");
|
||
}, 2200);
|
||
}
|
||
} else {
|
||
el.innerHTML = "";
|
||
if (toast) toast.classList.remove("show");
|
||
}
|
||
}
|
||
|
||
// === ACTIONS ===
|
||
function startRound() {
|
||
state.round = 0;
|
||
state.roundResults = [];
|
||
nextQuestion();
|
||
}
|
||
function nextQuestion() {
|
||
if (state.round >= ROUND_SIZE) {
|
||
finishRound();
|
||
return;
|
||
}
|
||
state.currentIdiom = pickIdiom();
|
||
state.blankIndices = generateBlanks(state.currentIdiom.w, state.difficulty);
|
||
state.selectedBlank = state.blankIndices[0];
|
||
state.filledChars = {};
|
||
state.hintUsed = false;
|
||
state.answered = false;
|
||
currentOptions = generateOptions(state.currentIdiom, state.blankIndices);
|
||
document.getElementById("btnNext").style.display = "none";
|
||
document.getElementById("btnHint").style.display = "";
|
||
document.getElementById("btnHint").style.opacity = "1";
|
||
document.getElementById("btnHint").style.pointerEvents = "";
|
||
document.getElementById("btnSkip").style.display = "";
|
||
document.getElementById("resultArea").innerHTML = "";
|
||
document.getElementById("optLabel").style.display = "";
|
||
renderProgress();
|
||
renderIdiom();
|
||
renderOptions(currentOptions);
|
||
renderCombo();
|
||
renderStats();
|
||
}
|
||
function selectBlank(i) {
|
||
if (state.answered) return;
|
||
if (state.filledChars[i]) {
|
||
delete state.filledChars[i];
|
||
state.selectedBlank = i;
|
||
sfxClick();
|
||
renderIdiom();
|
||
renderOptions(currentOptions);
|
||
return;
|
||
}
|
||
state.selectedBlank = i;
|
||
sfxClick();
|
||
renderIdiom();
|
||
}
|
||
function selectOption(ch) {
|
||
if (state.answered || state.selectedBlank === -1) return;
|
||
sfxClick();
|
||
state.filledChars[state.selectedBlank] = ch;
|
||
var next = -1;
|
||
for (var j = 0; j < state.blankIndices.length; j++) {
|
||
if (!state.filledChars[state.blankIndices[j]]) {
|
||
next = state.blankIndices[j];
|
||
break;
|
||
}
|
||
}
|
||
state.selectedBlank = next;
|
||
renderIdiom();
|
||
renderOptions(currentOptions);
|
||
var allFilled = true;
|
||
for (var j = 0; j < state.blankIndices.length; j++) {
|
||
if (!state.filledChars[state.blankIndices[j]]) {
|
||
allFilled = false;
|
||
break;
|
||
}
|
||
}
|
||
if (allFilled) checkAnswer();
|
||
}
|
||
function checkAnswer() {
|
||
state.answered = true;
|
||
var idiom = state.currentIdiom,
|
||
ok = true;
|
||
state.blankIndices.forEach(function (i) {
|
||
if (state.filledChars[i] !== idiom.w[i]) ok = false;
|
||
});
|
||
var boxes = document.querySelectorAll(".char-box");
|
||
state.blankIndices.forEach(function (i) {
|
||
boxes[i].classList.remove("filled");
|
||
boxes[i].classList.add(
|
||
state.filledChars[i] === idiom.w[i] ? "correct" : "wrong",
|
||
);
|
||
});
|
||
if (ok) {
|
||
state.combo++;
|
||
if (state.combo > state.maxCombo) state.maxCombo = state.combo;
|
||
state.score += Math.round(
|
||
state.difficulty *
|
||
10 *
|
||
(state.hintUsed ? 0.5 : 1) *
|
||
(1 + state.combo * 0.1),
|
||
);
|
||
state.todayCount++;
|
||
state.totalCount++;
|
||
state.roundResults.push(true);
|
||
state.combo >= 5 ? sfxCombo() : sfxCorrect();
|
||
if (state.reviewMode) removeFromWrongBook(idiom.w);
|
||
var nc = tryCheckin();
|
||
showResult(true, nc);
|
||
} else {
|
||
state.combo = 0;
|
||
state.roundResults.push(false);
|
||
sfxWrong();
|
||
addToWrongBook(idiom);
|
||
setTimeout(function () {
|
||
state.blankIndices.forEach(function (i) {
|
||
boxes[i].textContent = idiom.w[i];
|
||
boxes[i].classList.remove("wrong");
|
||
boxes[i].classList.add("correct");
|
||
});
|
||
}, 800);
|
||
showResult(false, false);
|
||
}
|
||
renderOptions(currentOptions);
|
||
renderCombo();
|
||
renderStats();
|
||
renderProgress();
|
||
saveTodayState();
|
||
saveState();
|
||
document.getElementById("btnNext").style.display = "";
|
||
document.getElementById("btnHint").style.display = "none";
|
||
document.getElementById("btnSkip").style.display = "none";
|
||
}
|
||
function showResult(ok, nc) {
|
||
var idiom = state.currentIdiom,
|
||
a = document.getElementById("resultArea");
|
||
var ch = "";
|
||
if (nc)
|
||
ch =
|
||
'<div style="margin-top:12px;padding:10px;background:linear-gradient(135deg,#FFF8E1,#FFECB3);border-radius:10px;text-align:center"><span style="font-size:1.2rem">🎉</span> <b>梅子今日打卡成功!</b> 连续 ' +
|
||
state.streak +
|
||
" 天</div>";
|
||
var pg = "";
|
||
if (!nc && state.todayCount < DAILY_GOAL)
|
||
pg =
|
||
'<div style="margin-top:8px;font-size:.8rem;color:var(--text2)">今日进度:' +
|
||
state.todayCount +
|
||
"/" +
|
||
DAILY_GOAL +
|
||
" 题(完成" +
|
||
DAILY_GOAL +
|
||
"题即可打卡)</div>";
|
||
a.innerHTML =
|
||
'<div class="result-card' +
|
||
(ok ? "" : " wrong-result") +
|
||
'"><h3 style="color:' +
|
||
(ok ? "var(--green)" : "var(--red)") +
|
||
'">' +
|
||
(ok ? "✅ 回答正确!" : "❌ 答错了~") +
|
||
(ok && state.combo >= 3 ? " 🔥" : "") +
|
||
'</h3><div class="idiom-word">' +
|
||
idiom.w +
|
||
'</div><div class="idiom-pinyin">' +
|
||
idiom.p +
|
||
'</div><div class="idiom-explain"><b>释义:</b>' +
|
||
idiom.e +
|
||
"</div>" +
|
||
ch +
|
||
pg +
|
||
"</div>";
|
||
}
|
||
function finishRound() {
|
||
var c = state.roundResults.filter(function (r) {
|
||
return r;
|
||
}).length;
|
||
sfxComplete();
|
||
var wasReview = state.reviewMode;
|
||
state.reviewMode = false;
|
||
var msg =
|
||
c === ROUND_SIZE
|
||
? "满分!梅子太厉害了!🎉"
|
||
: c >= 3
|
||
? "梅子表现不错!继续加油~"
|
||
: "没关系梅子,多练习就会进步!💪";
|
||
var reviewBtn =
|
||
wasReview && state.wrongBook.length
|
||
? '<button class="action-btn secondary" onclick="showWrongBookModal()" style="margin-top:8px;width:100%">再次练习错题 📖</button>'
|
||
: "";
|
||
document.getElementById("resultArea").innerHTML =
|
||
'<div class="result-card" style="border-top-color:var(--gold);text-align:center"><h3 style="color:var(--gold)">' +
|
||
(wasReview ? "📖 错题练习结束!" : "🏆 本轮结束!") +
|
||
'</h3><div style="font-size:2rem;font-weight:900;color:var(--accent);margin:12px 0;font-family:ZCOOL KuaiLe,cursive">' +
|
||
c +
|
||
" / " +
|
||
ROUND_SIZE +
|
||
'</div><div style="color:var(--text2);margin-bottom:16px">' +
|
||
msg +
|
||
'</div><button class="action-btn primary" onclick="startRound()" style="font-size:1rem;padding:12px 0;width:100%">再来一轮 →</button>' +
|
||
reviewBtn +
|
||
"</div>";
|
||
document.getElementById("btnNext").style.display = "none";
|
||
document.getElementById("btnHint").style.display = "none";
|
||
document.getElementById("btnSkip").style.display = "none";
|
||
document.getElementById("idiomDisplay").innerHTML = "";
|
||
document.getElementById("optionsGrid").innerHTML = "";
|
||
document.getElementById("optLabel").style.display = "none";
|
||
}
|
||
function useHint() {
|
||
if (state.answered || state.hintUsed) return;
|
||
state.hintUsed = true;
|
||
var ub = [];
|
||
for (var j = 0; j < state.blankIndices.length; j++) {
|
||
if (!state.filledChars[state.blankIndices[j]])
|
||
ub.push(state.blankIndices[j]);
|
||
}
|
||
if (!ub.length) return;
|
||
var hi = ub[0],
|
||
pp = state.currentIdiom.p.split(" "),
|
||
boxes = document.querySelectorAll(".char-box"),
|
||
py = document.createElement("span");
|
||
py.className = "pinyin-hint";
|
||
py.textContent = pp[hi] || "";
|
||
py.style.color = "var(--accent)";
|
||
boxes[hi].appendChild(py);
|
||
sfxClick();
|
||
document.getElementById("btnHint").style.opacity = ".5";
|
||
document.getElementById("btnHint").style.pointerEvents = "none";
|
||
}
|
||
function skipQuestion() {
|
||
if (state.answered) return;
|
||
state.combo = 0;
|
||
state.roundResults.push(false);
|
||
state.round++;
|
||
renderCombo();
|
||
renderProgress();
|
||
nextQuestion();
|
||
}
|
||
function showCheckinModal() {
|
||
document.getElementById("checkinModal").classList.add("show");
|
||
document.getElementById("modalStreak").textContent = state.streak;
|
||
var tc = document.getElementById("modalTodayCount");
|
||
var gf = document.getElementById("modalGoalFill");
|
||
if (tc) tc.textContent = state.todayCount;
|
||
if (gf)
|
||
gf.style.width =
|
||
Math.min(100, Math.round((state.todayCount / DAILY_GOAL) * 100)) + "%";
|
||
var cal = document.getElementById("modalCalendar");
|
||
cal.innerHTML = "";
|
||
var today = new Date(),
|
||
ts = getToday();
|
||
["一", "二", "三", "四", "五", "六", "日"].forEach(function (l) {
|
||
var d = document.createElement("div");
|
||
d.style.cssText =
|
||
"font-size:.65rem;color:#8B6914;display:flex;align-items:center;justify-content:center";
|
||
d.textContent = l;
|
||
cal.appendChild(d);
|
||
});
|
||
var start = new Date(today);
|
||
start.setDate(start.getDate() - 27);
|
||
while (start.getDay() !== 1) start.setDate(start.getDate() - 1);
|
||
var end = new Date(today);
|
||
end.setDate(end.getDate() + ((7 - today.getDay()) % 7));
|
||
var cur = new Date(start);
|
||
while (cur <= end) {
|
||
var ds = cur.toISOString().slice(0, 10),
|
||
d = document.createElement("div");
|
||
d.className = "cal-day";
|
||
d.textContent = cur.getDate();
|
||
if (state.checkinDays.indexOf(ds) !== -1) d.classList.add("checked");
|
||
if (ds === ts) d.classList.add("today");
|
||
if (cur > today) d.style.opacity = ".3";
|
||
cal.appendChild(d);
|
||
cur.setDate(cur.getDate() + 1);
|
||
}
|
||
}
|
||
|
||
// === INIT ===
|
||
function init() {
|
||
loadState();
|
||
loadWrongBook();
|
||
renderWbBadge();
|
||
if (state.bigFont) document.body.classList.add("big-font");
|
||
document.getElementById("btnSound").classList.toggle("active", state.sound);
|
||
document.getElementById("btnSound").textContent = state.sound ? "🔊" : "🔇";
|
||
if (state.bigFont)
|
||
document.getElementById("btnBigFont").classList.add("active");
|
||
document.querySelectorAll(".diff-btn").forEach(function (b) {
|
||
b.classList.toggle("active", +b.dataset.diff === state.difficulty);
|
||
});
|
||
renderStats();
|
||
startRound();
|
||
setTimeout(checkWhatsNew, 800);
|
||
}
|
||
document.getElementById("btnSound").addEventListener("click", function () {
|
||
state.sound = !state.sound;
|
||
this.classList.toggle("active", state.sound);
|
||
this.textContent = state.sound ? "🔊" : "🔇";
|
||
saveState();
|
||
});
|
||
document.getElementById("btnBigFont").addEventListener("click", function () {
|
||
state.bigFont = !state.bigFont;
|
||
document.body.classList.toggle("big-font", state.bigFont);
|
||
this.classList.toggle("active", state.bigFont);
|
||
saveState();
|
||
});
|
||
document
|
||
.getElementById("btnCheckin")
|
||
.addEventListener("click", showCheckinModal);
|
||
document.getElementById("closeModal").addEventListener("click", function () {
|
||
document.getElementById("checkinModal").classList.remove("show");
|
||
});
|
||
document.getElementById("checkinModal").addEventListener("click", function (e) {
|
||
if (e.target === document.getElementById("checkinModal"))
|
||
document.getElementById("checkinModal").classList.remove("show");
|
||
});
|
||
document
|
||
.getElementById("btnWrongBook")
|
||
.addEventListener("click", showWrongBookModal);
|
||
document
|
||
.getElementById("closeWrongBook")
|
||
.addEventListener("click", function () {
|
||
document.getElementById("wrongBookModal").classList.remove("show");
|
||
});
|
||
document
|
||
.getElementById("wrongBookModal")
|
||
.addEventListener("click", function (e) {
|
||
if (e.target === document.getElementById("wrongBookModal"))
|
||
document.getElementById("wrongBookModal").classList.remove("show");
|
||
});
|
||
document
|
||
.getElementById("btnStartReview")
|
||
.addEventListener("click", startReview);
|
||
document.getElementById("closeWhatsNew").addEventListener("click", function () {
|
||
document.getElementById("whatsNewModal").classList.remove("show");
|
||
localStorage.setItem("chengyu_v", APP_VERSION);
|
||
});
|
||
document
|
||
.getElementById("whatsNewModal")
|
||
.addEventListener("click", function (e) {
|
||
if (e.target === document.getElementById("whatsNewModal")) {
|
||
document.getElementById("whatsNewModal").classList.remove("show");
|
||
localStorage.setItem("chengyu_v", APP_VERSION);
|
||
}
|
||
});
|
||
document.querySelectorAll(".diff-btn").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
document.querySelectorAll(".diff-btn").forEach(function (x) {
|
||
x.classList.remove("active");
|
||
});
|
||
this.classList.add("active");
|
||
state.difficulty = +this.dataset.diff;
|
||
state.combo = 0;
|
||
saveState();
|
||
startRound();
|
||
});
|
||
});
|
||
document.getElementById("btnHint").addEventListener("click", useHint);
|
||
document.getElementById("btnSkip").addEventListener("click", skipQuestion);
|
||
document.getElementById("btnNext").addEventListener("click", function () {
|
||
state.round++;
|
||
document.getElementById("optLabel").style.display = "";
|
||
nextQuestion();
|
||
});
|
||
init();
|