// 猜谜活动前端模块 // 监听 RiddleGameStarted / RiddleGameAnswered 事件,提供答题弹窗与刷新恢复能力。 function csrf() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ""; } let currentRoundId = 0; let currentRoomId = 0; let currentQuizType = "idiom"; const QUIZ_TYPES = ["idiom", "brain_teaser"]; const QUIZ_INLINE_BUTTON_FONT_SIZE = "0.82em"; const QUIZ_INLINE_META_FONT_SIZE = "0.78em"; /** * 兼容新旧字段,提取前端统一使用的猜谜活动回合信息。 */ export function normalizeQuizRoundPayload(payload) { const source = payload && typeof payload === "object" ? payload : {}; const quizType = String(source.quiz_type || source.idiom_type || "idiom"); const quizTypeLabel = String(source.quiz_type_label || source.idiom_type_label || (quizType === "idiom" ? "成语题" : "谜题")); const roundId = Number.parseInt( String(source.quiz_round_id || source.idiom_game_round_id || source.idom_game_round_id || source.round_id || source.quiz_round_ended_id || source.idiom_game_round_ended_id || "0"), 10, ); const endedRoundId = Number.parseInt( String(source.quiz_round_ended_id || source.idiom_game_round_ended_id || "0"), 10, ); const rewardGold = Number.parseInt( String(source.quiz_reward_gold ?? source.idiom_reward_gold ?? source.idiom_result_reward_gold ?? source.reward_gold ?? 0), 10, ); const rewardExp = Number.parseInt( String(source.quiz_reward_exp ?? source.idiom_reward_exp ?? source.idiom_result_reward_exp ?? source.reward_exp ?? 0), 10, ); const hint = String(source.quiz_hint || source.hint || source.content || ""); const answer = String(source.quiz_answer || source.idiom_answer || source.answer || ""); return { quizType, quizTypeLabel, roundId: Number.isNaN(roundId) ? 0 : roundId, endedRoundId: Number.isNaN(endedRoundId) ? 0 : endedRoundId, rewardGold: Number.isNaN(rewardGold) ? 0 : rewardGold, rewardExp: Number.isNaN(rewardExp) ? 0 : rewardExp, hint, answer, }; } /** * 统一构建“猜谜活动 + 题型”展示标题。 */ export function buildQuizActivityTitle(payload) { const quizMeta = normalizeQuizRoundPayload(payload); return { activityLabel: "猜谜活动", typeLabel: quizMeta.quizTypeLabel || "谜题", quizType: quizMeta.quizType, }; } /** * 判断一条消息是否属于开题消息。 */ export function isQuizStartMessage(payload) { const quizMeta = normalizeQuizRoundPayload(payload); const action = String(payload?.action || ""); return quizMeta.roundId > 0 && quizMeta.endedRoundId <= 0 && !action; } /** * 查找当前回合是否已经有对应的聊天室消息节点。 */ function findIdiomRoundMessageNode(roundId) { if (roundId <= 0) { return null; } return document.querySelector(`[data-idiom-round-id="${roundId}"]`); } /** * 刷新后若历史消息里缺少当前进行中的开题卡片,则主动补回一条系统传音消息。 */ function restoreCurrentQuizMessage(roomId, payload) { const quizMeta = normalizeQuizRoundPayload(payload); if (quizMeta.roundId <= 0 || !quizMeta.hint) { return; } if (findIdiomRoundMessageNode(quizMeta.roundId)) { return; } const { activityLabel, typeLabel } = buildQuizActivityTitle(payload); const now = new Date(); const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`; window.appendMessage?.({ id: `quiz-start-restore-${quizMeta.roundId}`, room_id: roomId, from_user: "系统传音", to_user: "大家", content: `🧩 【${activityLabel}|${typeLabel}】${quizMeta.hint}`, is_secret: false, font_color: "#7c3aed", action: "", quiz_type: quizMeta.quizType, quiz_type_label: typeLabel, quiz_round_id: quizMeta.roundId, quiz_hint: quizMeta.hint, quiz_reward_gold: quizMeta.rewardGold, quiz_reward_exp: quizMeta.rewardExp, idiom_game_round_id: quizMeta.roundId, idiom_reward_gold: quizMeta.rewardGold, idiom_reward_exp: quizMeta.rewardExp, sent_at: timeStr, }); } /** * 为指定回合创建统一样式的答题按钮。 */ function buildIdiomAnswerButton(roundId, hint, rewardGold, rewardExp, typeLabel, quizType = "idiom") { const btn = document.createElement("button"); btn.type = "button"; btn.dataset.idiomAnswerBtn = String(roundId); btn.dataset.quizAnswerBtn = String(roundId); btn.dataset.idiomHint = hint; btn.dataset.quizHint = hint; btn.dataset.idiomGold = String(rewardGold); btn.dataset.quizGold = String(rewardGold); btn.dataset.idiomExp = String(rewardExp); btn.dataset.quizExp = String(rewardExp); btn.dataset.quizTypeLabel = typeLabel; btn.dataset.quizType = quizType; btn.dataset.quizEnded = "0"; btn.textContent = "🎯 立即答题"; btn.style.cssText = "display:inline-flex;align-items:center;gap:4px;padding:2px 9px;background:linear-gradient(135deg,#7c3aed,#a78bfa);" + `color:#fff;border:1px solid #7c3aed;border-radius:999px;font-size:${QUIZ_INLINE_BUTTON_FONT_SIZE};cursor:pointer;` + "font-weight:700;line-height:1;vertical-align:middle;box-shadow:0 2px 6px rgba(124,58,237,.14);"; return btn; } /** * 查找指定回合的所有答题按钮。 */ function queryQuizAnswerButtons(roundId = 0) { const selector = roundId > 0 ? `[data-quiz-answer-btn="${roundId}"], [data-idiom-answer-btn="${roundId}"]` : "[data-quiz-answer-btn], [data-idiom-answer-btn]"; return Array.from(document.querySelectorAll(selector)); } /** * 读取当前页面上该回合已渲染的结算消息,用于历史恢复时补挂答对人名字。 */ function findQuizWinnerUsername(roundId = 0) { if (roundId <= 0) { return ""; } const resultNode = document.querySelector(`[data-quiz-round-ended-id="${roundId}"]`); return String(resultNode?.dataset?.quizWinnerUsername || ""); } /** * 清理指定回合的所有答题按钮。 */ export function removeIdiomAnswerButtons(roundId = 0) { queryQuizAnswerButtons(roundId).forEach((button) => button.remove()); } /** * 为结束态按钮补一个答对人标记,避免用户只看到“已结束”不知道是谁抢到了。 */ function syncQuizWinnerLabel(button, winnerUsername = "") { if (!(button instanceof HTMLElement)) { return; } const existingLabel = button.parentElement?.querySelector(`[data-quiz-winner-label="${button.dataset.quizAnswerBtn || button.dataset.idiomAnswerBtn || ""}"]`); if (!winnerUsername) { existingLabel?.remove(); return; } const winnerLabel = existingLabel || document.createElement("span"); winnerLabel.dataset.quizWinnerLabel = String(button.dataset.quizAnswerBtn || button.dataset.idiomAnswerBtn || "0"); winnerLabel.textContent = `答对:${winnerUsername}`; winnerLabel.style.cssText = `margin-left:6px;font-size:${QUIZ_INLINE_META_FONT_SIZE};line-height:1.2;color:#64748b;font-weight:700;white-space:nowrap;`; if (!existingLabel) { button.insertAdjacentElement("afterend", winnerLabel); } } /** * 将指定回合的答题按钮标记为结束态,保留在历史消息中供用户回看。 */ export function disableIdiomAnswerButtons(roundId = 0, endedText = "本回合已结束", winnerUsername = "") { queryQuizAnswerButtons(roundId).forEach((button) => { button.disabled = true; button.dataset.quizEnded = "1"; button.style.background = "linear-gradient(135deg,#94a3b8,#cbd5e1)"; button.style.color = "#f8fafc"; button.style.border = "1px solid #94a3b8"; button.style.cursor = "not-allowed"; button.style.boxShadow = "none"; button.style.opacity = ".92"; button.style.padding = "2px 9px"; button.style.fontSize = QUIZ_INLINE_BUTTON_FONT_SIZE; button.style.lineHeight = "1"; button.title = endedText; button.textContent = "已结束"; syncQuizWinnerLabel(button, winnerUsername); }); } /** * 根据当前回合状态同步按钮可点击性,避免刷新后仍显示过期入口。 */ function syncQuizAnswerButtons(activeRoundIds) { const activeIds = new Set((Array.isArray(activeRoundIds) ? activeRoundIds : [activeRoundIds]).filter((roundId) => roundId > 0)); queryQuizAnswerButtons().forEach((button) => { const buttonRoundId = Number.parseInt(String(button.dataset.quizAnswerBtn || button.dataset.idiomAnswerBtn || "0"), 10); if (activeIds.has(buttonRoundId)) { button.disabled = false; button.dataset.quizEnded = "0"; button.style.background = "linear-gradient(135deg,#7c3aed,#a78bfa)"; button.style.color = "#fff"; button.style.border = "1px solid #7c3aed"; button.style.cursor = "pointer"; button.style.boxShadow = "0 2px 6px rgba(124,58,237,.14)"; button.style.opacity = "1"; button.style.padding = "2px 9px"; button.style.fontSize = QUIZ_INLINE_BUTTON_FONT_SIZE; button.style.lineHeight = "1"; button.title = ""; button.textContent = "🎯 立即答题"; syncQuizWinnerLabel(button, ""); return; } disableIdiomAnswerButtons(buttonRoundId); }); } /** * 把答题按钮挂到对应的消息节点上,而不是盲目追加到最后一条消息。 */ export function attachIdiomAnswerButton(messageNode, message) { if (!messageNode || !message) { return; } const quizMeta = normalizeQuizRoundPayload(message); const roundId = quizMeta.endedRoundId || quizMeta.roundId; if (roundId <= 0) { return; } if (quizMeta.endedRoundId > 0) { return; } if (!["星海小博士", "系统传音"].includes(String(message.from_user || ""))) { return; } if (messageNode.querySelector(`[data-quiz-answer-btn="${roundId}"], [data-idiom-answer-btn="${roundId}"]`)) { return; } const button = buildIdiomAnswerButton(roundId, quizMeta.hint, quizMeta.rewardGold, quizMeta.rewardExp, quizMeta.quizTypeLabel, quizMeta.quizType); const inlineActionAnchor = messageNode.querySelector("[data-quiz-inline-action-anchor]"); if (inlineActionAnchor?.parentNode) { inlineActionAnchor.parentNode.insertBefore(button, inlineActionAnchor.nextSibling); } else { messageNode.appendChild(button); } if (quizMeta.endedRoundId > 0) { disableIdiomAnswerButtons(roundId); return; } const winnerUsername = findQuizWinnerUsername(roundId); if (winnerUsername) { disableIdiomAnswerButtons(roundId, "本回合已结束", winnerUsername); } } /** * 根据当前服务端回合状态,清理刷新后残留的旧答题按钮。 */ async function syncCurrentIdiomRound() { const roomId = Number.parseInt(String(window.chatContext?.roomId || "0"), 10); if (roomId <= 0) { return; } currentRoomId = roomId; try { const responses = await Promise.all(QUIZ_TYPES.map(async (quizType) => { const response = await fetch(`/riddle-quiz/current?room_id=${roomId}&type=${encodeURIComponent(quizType)}`, { headers: { Accept: "application/json", }, }); return response.json(); })); responses.forEach((data) => { if (data?.status === "success" && data?.data) { restoreCurrentQuizMessage(roomId, data.data); } }); const activeRoundIds = responses .map((data) => Number.parseInt(String(data?.data?.quiz_round_id || data?.data?.round_id || "0"), 10)) .filter((roundId) => roundId > 0); currentRoundId = activeRoundIds[0] || 0; currentQuizType = responses.find((data) => Number.parseInt(String(data?.data?.quiz_round_id || data?.data?.round_id || "0"), 10) === currentRoundId)?.data?.quiz_type || currentQuizType; syncQuizAnswerButtons(activeRoundIds); } catch (_error) { // 当前回合同步失败时不打断聊天主流程,保留现有按钮状态。 } } /** * 收到猜成语出题事件时,在聊天窗口显示提示消息。 */ function handleRiddleGameStarted(e) { const quizMeta = normalizeQuizRoundPayload(e.detail); const { activityLabel, typeLabel } = buildQuizActivityTitle(e.detail); const { roundId, hint, rewardGold, rewardExp } = quizMeta; const message = String(e.detail?.message || ""); if (!roundId || !hint) return; currentRoundId = roundId; currentRoomId = window.chatContext?.roomId || 0; currentQuizType = quizMeta.quizType || "idiom"; // 线上如果 MessageSent 补消息没有到达,这里主动补一条公屏消息兜底; // 本地或正常链路下若消息已存在,则只补挂答题按钮,避免重复渲染。 const existingMessageNode = findIdiomRoundMessageNode(roundId); if (existingMessageNode) { attachIdiomAnswerButton(existingMessageNode, { from_user: "星海小博士", content: message || `🧩 【${activityLabel}|${typeLabel}】${hint}`, quiz_type: quizMeta.quizType, quiz_type_label: typeLabel, quiz_round_id: roundId, quiz_reward_gold: rewardGold, quiz_reward_exp: rewardExp, idiom_game_round_id: roundId, idiom_reward_gold: rewardGold, idiom_reward_exp: rewardExp, }); console.log(`猜谜活动开始:${hint},奖励 ${rewardGold}金/${rewardExp}经验`); return; } const now = new Date(); const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`; window.appendMessage?.({ id: `quiz-start-live-${roundId}`, room_id: currentRoomId || window.chatContext?.roomId || 0, from_user: "系统传音", to_user: "大家", content: message || `🧩 【${activityLabel}|${typeLabel}】${hint}`, is_secret: false, font_color: "#7c3aed", action: "", quiz_type: quizMeta.quizType, quiz_type_label: typeLabel, quiz_round_id: roundId, quiz_reward_gold: rewardGold, quiz_reward_exp: rewardExp, idiom_game_round_id: roundId, idiom_reward_gold: rewardGold, idiom_reward_exp: rewardExp, sent_at: timeStr, }); console.log(`猜谜活动开始:${hint},奖励 ${rewardGold}金/${rewardExp}经验`); } /** * 收到猜成语结果事件。 */ function handleRiddleGameAnswered(e) { const quizMeta = normalizeQuizRoundPayload(e.detail); const { activityLabel, typeLabel } = buildQuizActivityTitle(e.detail); const answer = quizMeta.answer; const winnerUsername = String(e.detail?.winner_username || ""); const rewardGold = quizMeta.rewardGold; const rewardExp = quizMeta.rewardExp; const roundId = quizMeta.endedRoundId || quizMeta.roundId; if (!answer) return; currentRoundId = 0; currentQuizType = "idiom"; disableIdiomAnswerButtons(roundId, "本回合已结束", winnerUsername); // 关闭当前用户的答题弹窗(如果开着的话) const answerModal = document.getElementById("idiom-answer-modal"); if (answerModal && answerModal.style.display !== "none") { answerModal.style.display = "none"; } // 实时答题结果与刷新后的历史恢复统一走 appendMessage,避免两套分流逻辑跑偏。 const now = new Date(); const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`; window.appendMessage?.({ id: `quiz-result-live-${roundId}-${Date.now()}`, room_id: currentRoomId || window.chatContext?.roomId || 0, from_user: "系统传音", to_user: "大家", content: `🎉 【${winnerUsername}】率先答对${typeLabel}「${answer}」,获得 ${rewardGold} 金币、${rewardExp} 经验!`, is_secret: false, font_color: "#16a34a", action: "idiom_result", winner_username: winnerUsername, quiz_type: quizMeta.quizType, quiz_type_label: typeLabel, quiz_answer: answer, quiz_reward_gold: rewardGold, quiz_reward_exp: rewardExp, quiz_round_ended_id: roundId, idiom_answer: answer, idiom_result_reward_gold: rewardGold, idiom_result_reward_exp: rewardExp, idiom_game_round_ended_id: roundId, sent_at: timeStr, }); // ── Toast 通知(所有用户都能看到) ── window.chatToast?.show({ title: `🧩 ${activityLabel} · ${typeLabel}`, message: `${winnerUsername} 答对了「${answer}」,获得 ${rewardGold}💰 + ${rewardExp}⭐!`, icon: "🎉", color: "#16a34a", duration: 6000, }); } /** * 打开答题弹窗。 */ function openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp, typeLabel = "成语题", quizType = "idiom") { currentRoundId = roundId; currentRoomId = window.chatContext?.roomId || 0; currentQuizType = quizType || "idiom"; const modal = document.getElementById("idiom-answer-modal"); if (!modal) return; const hintEl = document.getElementById("idiom-answer-hint"); const rewardEl = document.getElementById("idiom-answer-reward"); const typeEl = document.getElementById("idiom-answer-type"); if (hintEl) hintEl.textContent = hint; if (rewardEl) rewardEl.textContent = `🎁 答对奖励:${rewardGold} 金币 + ${rewardExp} 经验`; if (typeEl) typeEl.textContent = typeLabel; 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("/riddle-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, quiz_type: currentQuizType, }), }); const data = await response.json(); if (data.status === "success") { feedbackEl.textContent = data.message || "🎉 回答正确!"; feedbackEl.style.color = "#16a34a"; input.disabled = true; disableIdiomAnswerButtons( currentRoundId, "本回合已结束", String(window.chatContext?.username || ""), ); // 延迟关闭弹窗 setTimeout(() => { closeIdiomAnswerModal(); }, 2000); } else { feedbackEl.textContent = data.message || "答案不正确"; feedbackEl.style.color = "#ef4444"; if ((data.message || "").includes("已结束") || (data.message || "").includes("抢先答对") || (data.message || "").includes("超时")) { disableIdiomAnswerButtons(currentRoundId, data.message || "本回合已结束"); } 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; if (btn instanceof HTMLButtonElement && (btn.disabled || btn.dataset.quizEnded === "1")) { return; } const roundId = parseInt(btn.dataset.quizAnswerBtn || btn.dataset.idiomAnswerBtn || "0", 10); const hint = btn.dataset.quizHint || btn.dataset.idiomHint || ""; const rewardGold = parseInt(btn.dataset.quizGold || btn.dataset.idiomGold || "0", 10); const rewardExp = parseInt(btn.dataset.quizExp || btn.dataset.idiomExp || "0", 10); const typeLabel = btn.dataset.quizTypeLabel || "成语题"; currentQuizType = btn.dataset.quizType || (typeLabel === "脑筋急转弯" ? "brain_teaser" : "idiom"); if (roundId > 0) { openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp, typeLabel, currentQuizType); } }); // ── 猜成语结果消息中的用户名可点击 → 打开用户名片 // 注:单击/双击已由 right-panel.js 的全局 [data-chat-message-user] 事件委托统一处理 window.setTimeout(() => { syncCurrentIdiomRound(); }, 0); } // ── 挂载到 window ── window.openIdiomAnswerModal = openIdiomAnswerModal; window.closeIdiomAnswerModal = closeIdiomAnswerModal; window.submitIdiomAnswer = submitIdiomAnswer; window.handleRiddleGameStarted = handleRiddleGameStarted; window.handleRiddleGameAnswered = handleRiddleGameAnswered;