feat: 猜成语游戏 - 完整题库、管理后台、答题弹窗
- 创建 idioms 表(102条谜语式成语题库)和 idiom_game_rounds 表 - 后台成语管理页面:增删改题目 + 游戏参数(金币/经验/间隔)内联设置 + 出题按钮 - IdiomQuizController:出题/答题/当前回合查询,Redis 防并发抢答 - IdiomGameStarted / IdiomGameAnswered 广播事件 - 前端答题弹窗模块:聊天消息带【答题】按钮,点击弹出输入框 - GameConfig 注册 idiom 游戏,由 admin.game-configs 统一管理开关
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user