feat: 猜成语游戏 - 完整题库、管理后台、答题弹窗

- 创建 idioms 表(102条谜语式成语题库)和 idiom_game_rounds 表
- 后台成语管理页面:增删改题目 + 游戏参数(金币/经验/间隔)内联设置 + 出题按钮
- IdiomQuizController:出题/答题/当前回合查询,Redis 防并发抢答
- IdiomGameStarted / IdiomGameAnswered 广播事件
- 前端答题弹窗模块:聊天消息带【答题】按钮,点击弹出输入框
- GameConfig 注册 idiom 游戏,由 admin.game-configs 统一管理开关
This commit is contained in:
pllx
2026-04-28 23:42:48 +08:00
parent 461c6a6f56
commit 4ff62e29bd
20 changed files with 1497 additions and 1 deletions
+5
View File
@@ -291,6 +291,10 @@ import { bindChatInitialStateControls } from "./chat-room/initial-state.js";
// 拍一拍模块
import "./chat-room/pat.js";
// 猜成语游戏模块
import "./chat-room/idiom-quiz.js";
import { bindIdiomQuizControls } from "./chat-room/idiom-quiz.js";
// 斜杠命令菜单
import { bindSlashCommands, registerSlashCommand } from "./chat-room/slash-commands.js";
@@ -778,4 +782,5 @@ if (typeof window !== "undefined") {
bindChatBotControls();
bindGuestbookControls();
bindFeedbackControls();
bindIdiomQuizControls();
}
+49
View File
@@ -366,6 +366,41 @@ export function bindChatEvents() {
}
enqueueChatMessage(msg);
// 猜成语消息:追加【答题】按钮
if (msg.idom_game_round_id || msg.idiom_game_round_id) {
const roundId = msg.idom_game_round_id || msg.idiom_game_round_id;
const hint = msg.content || "";
const rewardGold = msg.idiom_reward_gold || 0;
const rewardExp = msg.idiom_reward_exp || 0;
// 延迟等消息渲染完成再追加按钮
setTimeout(() => {
const containers = [
document.getElementById("chat-messages-container"),
document.getElementById("chat-messages-container2"),
];
containers.forEach((container) => {
if (!container) return;
const lastMsg = container.lastElementChild;
if (!lastMsg || lastMsg.querySelector("[data-idiom-answer-btn]")) return;
if (lastMsg.dataset.fromUser !== "星海小博士") return;
const btn = document.createElement("button");
btn.type = "button";
btn.dataset.idiomAnswerBtn = String(roundId);
btn.dataset.idiomHint = hint;
btn.dataset.idiomGold = String(rewardGold);
btn.dataset.idiomExp = String(rewardExp);
btn.textContent = "🎯 答题";
btn.style.cssText =
"margin-left:8px;padding:2px 12px;background:linear-gradient(135deg,#7c3aed,#a78bfa);" +
"color:#fff;border:none;border-radius:999px;font-size:11px;cursor:pointer;" +
"font-weight:bold;vertical-align:middle;";
lastMsg.appendChild(btn);
});
}, 50);
}
if (msg.action === "vip_presence" && typeof window.showVipPresenceBanner === "function") {
window.showVipPresenceBanner(msg);
}
@@ -470,6 +505,20 @@ export function bindChatEvents() {
}
});
// chat:idiom-started — 猜成语出题
window.addEventListener("chat:idiom-started", (e) => {
if (typeof window.handleIdiomGameStarted === "function") {
window.handleIdiomGameStarted(e);
}
});
// chat:idiom-answered — 猜成语答题结果
window.addEventListener("chat:idiom-answered", (e) => {
if (typeof window.handleIdiomGameAnswered === "function") {
window.handleIdiomGameAnswered(e);
}
});
// Echo 级监听器(延迟绑定,等待 Echo 就绪)
document.addEventListener("DOMContentLoaded", () => {
setupScreenClearedListener();
+215
View File
@@ -0,0 +1,215 @@
// 猜成语游戏前端模块
// 监听 IdiomGameStarted / IdiomGameAnswered 事件,提供答题弹窗功能
function csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
}
let currentRoundId = 0;
let currentRoomId = 0;
/**
* 收到猜成语出题事件时,在聊天窗口显示提示消息。
*/
function handleIdiomGameStarted(e) {
const { round_id, hint, reward_gold, reward_exp, message } = e.detail || {};
if (!round_id || !hint) return;
currentRoundId = round_id;
currentRoomId = window.chatContext?.roomId || 0;
// 追加一条聊天室消息(由 MessageSent 事件负责渲染,不重复添加)
// 这里只存储当前回合信息
console.log(`猜成语开始:${hint},奖励 ${reward_gold}金/${reward_exp}经验`);
}
/**
* 收到猜成语结果事件。
*/
function handleIdiomGameAnswered(e) {
const { answer, winner_username, reward_gold, reward_exp } = e.detail || {};
if (!answer) return;
currentRoundId = 0;
// 如果当前用户打开答题弹窗但被别人抢先了,关闭弹窗
const answerModal = document.getElementById("idiom-answer-modal");
if (answerModal && answerModal.style.display !== "none") {
answerModal.style.display = "none";
window.chatToast?.show({
title: "被抢先了",
message: `${winner_username} 率先答对了「${answer}」,下次加油!`,
icon: "😅",
color: "#f59e0b",
duration: 4000,
});
}
}
/**
* 打开答题弹窗。
*/
function openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp) {
currentRoundId = roundId;
currentRoomId = window.chatContext?.roomId || 0;
const modal = document.getElementById("idiom-answer-modal");
if (!modal) return;
const hintEl = document.getElementById("idiom-answer-hint");
const rewardEl = document.getElementById("idiom-answer-reward");
if (hintEl) hintEl.textContent = hint;
if (rewardEl) rewardEl.textContent = `🎁 答对奖励:${rewardGold} 金币 + ${rewardExp} 经验`;
modal.style.display = "flex";
const input = document.getElementById("idiom-answer-input");
if (input) {
input.value = "";
input.focus();
input.disabled = false;
}
const submitBtn = document.getElementById("idiom-answer-submit");
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "提交答案";
}
const feedbackEl = document.getElementById("idiom-answer-feedback");
if (feedbackEl) feedbackEl.textContent = "";
}
/**
* 关闭答题弹窗。
*/
function closeIdiomAnswerModal() {
const modal = document.getElementById("idiom-answer-modal");
if (modal) modal.style.display = "none";
}
/**
* 提交答案。
*/
async function submitIdiomAnswer() {
const input = document.getElementById("idiom-answer-input");
const feedbackEl = document.getElementById("idiom-answer-feedback");
const submitBtn = document.getElementById("idiom-answer-submit");
if (!input || !feedbackEl || !submitBtn) return;
const answer = input.value.trim();
if (!answer) {
feedbackEl.textContent = "请输入成语答案";
feedbackEl.style.color = "#ef4444";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "提交中...";
try {
const response = await fetch("/idiom-quiz/answer", {
method: "POST",
headers: {
"X-CSRF-TOKEN": csrf(),
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({
round_id: currentRoundId,
answer: answer,
room_id: currentRoomId,
}),
});
const data = await response.json();
if (data.status === "success") {
feedbackEl.textContent = data.message || "🎉 回答正确!";
feedbackEl.style.color = "#16a34a";
input.disabled = true;
// 延迟关闭弹窗
setTimeout(() => {
closeIdiomAnswerModal();
}, 2000);
} else {
feedbackEl.textContent = data.message || "答案不正确";
feedbackEl.style.color = "#ef4444";
submitBtn.disabled = false;
submitBtn.textContent = "提交答案";
input.focus();
input.select();
}
} catch (error) {
feedbackEl.textContent = "网络错误,请稍后重试";
feedbackEl.style.color = "#ef4444";
submitBtn.disabled = false;
submitBtn.textContent = "提交答案";
}
}
// ── 事件绑定 ──
export function bindIdiomQuizControls() {
// 已经绑定的不再重复绑定
if (document.getElementById("idiom-answer-modal")?.dataset?.idiomBound) return;
const modal = document.getElementById("idiom-answer-modal");
if (modal) modal.dataset.idiomBound = "1";
// 关闭按钮
document.addEventListener("click", (e) => {
const closeBtn = e.target.closest("[data-idiom-answer-close]");
if (closeBtn) {
closeIdiomAnswerModal();
return;
}
// 点击遮罩层关闭
const overlay = e.target.closest("#idiom-answer-modal");
if (overlay && e.target === overlay) {
closeIdiomAnswerModal();
}
});
// 提交按钮
document.addEventListener("click", (e) => {
const submitBtn = e.target.closest("[data-idiom-answer-submit]");
if (submitBtn) {
e.preventDefault();
submitIdiomAnswer();
}
});
// 输入框 Enter 提交
document.addEventListener("keydown", (e) => {
const input = e.target.closest("#idiom-answer-input");
if (input && e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submitIdiomAnswer();
}
});
// 聊天消息中的【答题】按钮点击
document.addEventListener("click", (e) => {
const btn = e.target.closest("[data-idiom-answer-btn]");
if (!btn) return;
const roundId = parseInt(btn.dataset.idiomAnswerBtn || "0", 10);
const hint = btn.dataset.idiomHint || "";
const rewardGold = parseInt(btn.dataset.idiomGold || "0", 10);
const rewardExp = parseInt(btn.dataset.idiomExp || "0", 10);
if (roundId > 0) {
openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp);
}
});
}
// ── 挂载到 window ──
window.openIdiomAnswerModal = openIdiomAnswerModal;
window.closeIdiomAnswerModal = closeIdiomAnswerModal;
window.submitIdiomAnswer = submitIdiomAnswer;
window.handleIdiomGameStarted = handleIdiomGameStarted;
window.handleIdiomGameAnswered = handleIdiomGameAnswered;
+10
View File
@@ -269,6 +269,16 @@ export function initChat(roomId) {
console.log("拍一拍:", e);
window.dispatchEvent(new CustomEvent("chat:pat", { detail: e }));
})
// 监听猜成语出题
.listen("IdiomGameStarted", (e) => {
console.log("猜成语:", e);
window.dispatchEvent(new CustomEvent("chat:idiom-started", { detail: e }));
})
// 监听猜成语答题结果
.listen("IdiomGameAnswered", (e) => {
console.log("猜成语结果:", e);
window.dispatchEvent(new CustomEvent("chat:idiom-answered", { detail: e }));
})
// 监听任命公告(礼花 + 隆重弹窗)
.listen("AppointmentAnnounced", (e) => {
console.log("任命公告:", e);