first commit
This commit is contained in:
828
app.js
Normal file
828
app.js
Normal file
@@ -0,0 +1,828 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user