重构猜谜活动并统一聊天室答题通知
This commit is contained in:
@@ -292,8 +292,8 @@ 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 "./chat-room/riddle-quiz.js";
|
||||
import { bindIdiomQuizControls } from "./chat-room/riddle-quiz.js";
|
||||
|
||||
// 斜杠命令菜单
|
||||
import { bindSlashCommands, registerSlashCommand } from "./chat-room/slash-commands.js";
|
||||
|
||||
@@ -504,15 +504,15 @@ export function bindChatEvents() {
|
||||
|
||||
// chat:idiom-started — 猜成语出题
|
||||
window.addEventListener("chat:idiom-started", (e) => {
|
||||
if (typeof window.handleIdiomGameStarted === "function") {
|
||||
window.handleIdiomGameStarted(e);
|
||||
if (typeof window.handleRiddleGameStarted === "function") {
|
||||
window.handleRiddleGameStarted(e);
|
||||
}
|
||||
});
|
||||
|
||||
// chat:idiom-answered — 猜成语答题结果
|
||||
window.addEventListener("chat:idiom-answered", (e) => {
|
||||
if (typeof window.handleIdiomGameAnswered === "function") {
|
||||
window.handleIdiomGameAnswered(e);
|
||||
if (typeof window.handleRiddleGameAnswered === "function") {
|
||||
window.handleRiddleGameAnswered(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
// 猜成语游戏前端模块
|
||||
// 监听 IdiomGameStarted / IdiomGameAnswered 事件,提供答题弹窗功能
|
||||
|
||||
function csrf() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||
}
|
||||
|
||||
let currentRoundId = 0;
|
||||
let currentRoomId = 0;
|
||||
|
||||
/**
|
||||
* 查找当前回合是否已经有对应的聊天室消息节点。
|
||||
*/
|
||||
function findIdiomRoundMessageNode(roundId) {
|
||||
if (roundId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return document.querySelector(`[data-idiom-round-id="${roundId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定回合创建统一样式的答题按钮。
|
||||
*/
|
||||
function buildIdiomAnswerButton(roundId, hint, rewardGold, rewardExp) {
|
||||
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;";
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定回合的所有答题按钮。
|
||||
*/
|
||||
export function removeIdiomAnswerButtons(roundId = 0) {
|
||||
const selector = roundId > 0
|
||||
? `[data-idiom-answer-btn="${roundId}"]`
|
||||
: "[data-idiom-answer-btn]";
|
||||
|
||||
document.querySelectorAll(selector).forEach((button) => button.remove());
|
||||
}
|
||||
|
||||
/**
|
||||
* 把答题按钮挂到对应的消息节点上,而不是盲目追加到最后一条消息。
|
||||
*/
|
||||
export function attachIdiomAnswerButton(messageNode, message) {
|
||||
if (!messageNode || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roundId = Number.parseInt(
|
||||
String(message.idiom_game_round_id || message.idom_game_round_id || "0"),
|
||||
10,
|
||||
);
|
||||
if (roundId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.parseInt(String(message.idiom_game_round_ended_id || "0"), 10) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.from_user !== "星海小博士") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageNode.querySelector(`[data-idiom-answer-btn="${roundId}"]`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hint = String(message.content || "");
|
||||
const rewardGold = Number.parseInt(String(message.idiom_reward_gold || "0"), 10);
|
||||
const rewardExp = Number.parseInt(String(message.idiom_reward_exp || "0"), 10);
|
||||
const button = buildIdiomAnswerButton(roundId, hint, rewardGold, rewardExp);
|
||||
const timeNode = messageNode.querySelector(".msg-time");
|
||||
|
||||
if (timeNode?.parentNode) {
|
||||
timeNode.parentNode.insertBefore(button, timeNode.nextSibling);
|
||||
return;
|
||||
}
|
||||
|
||||
messageNode.appendChild(button);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前服务端回合状态,清理刷新后残留的旧答题按钮。
|
||||
*/
|
||||
async function syncCurrentIdiomRound() {
|
||||
const roomId = Number.parseInt(String(window.chatContext?.roomId || "0"), 10);
|
||||
if (roomId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentRoomId = roomId;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/idiom-quiz/current?room_id=${roomId}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const activeRoundId = Number.parseInt(String(data?.data?.round_id || "0"), 10);
|
||||
|
||||
currentRoundId = activeRoundId;
|
||||
|
||||
if (activeRoundId <= 0) {
|
||||
removeIdiomAnswerButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-idiom-answer-btn]").forEach((button) => {
|
||||
if (button.dataset.idiomAnswerBtn !== String(activeRoundId)) {
|
||||
button.remove();
|
||||
}
|
||||
});
|
||||
} catch (_error) {
|
||||
// 当前回合同步失败时不打断聊天主流程,保留现有按钮状态。
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到猜成语出题事件时,在聊天窗口显示提示消息。
|
||||
*/
|
||||
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 补消息没有到达,这里主动补一条公屏消息兜底;
|
||||
// 本地或正常链路下若消息已存在,则只补挂答题按钮,避免重复渲染。
|
||||
const existingMessageNode = findIdiomRoundMessageNode(round_id);
|
||||
if (existingMessageNode) {
|
||||
attachIdiomAnswerButton(existingMessageNode, {
|
||||
from_user: "星海小博士",
|
||||
content: message || `🧩 猜成语时间!${hint}`,
|
||||
idiom_game_round_id: round_id,
|
||||
idiom_reward_gold: reward_gold,
|
||||
idiom_reward_exp: reward_exp,
|
||||
});
|
||||
console.log(`猜成语开始:${hint},奖励 ${reward_gold}金/${reward_exp}经验`);
|
||||
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: `idiom-start-live-${round_id}`,
|
||||
room_id: currentRoomId || window.chatContext?.roomId || 0,
|
||||
from_user: "星海小博士",
|
||||
to_user: "大家",
|
||||
content: message || `🧩 猜成语时间!${hint}`,
|
||||
is_secret: false,
|
||||
font_color: "#7c3aed",
|
||||
action: "",
|
||||
idiom_game_round_id: round_id,
|
||||
idiom_reward_gold: reward_gold,
|
||||
idiom_reward_exp: reward_exp,
|
||||
sent_at: timeStr,
|
||||
});
|
||||
|
||||
console.log(`猜成语开始:${hint},奖励 ${reward_gold}金/${reward_exp}经验`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到猜成语结果事件。
|
||||
*/
|
||||
function handleIdiomGameAnswered(e) {
|
||||
const { answer, winner_username, reward_gold, reward_exp, round_id } = e.detail || {};
|
||||
if (!answer) return;
|
||||
|
||||
currentRoundId = 0;
|
||||
removeIdiomAnswerButtons(round_id);
|
||||
|
||||
// 关闭当前用户的答题弹窗(如果开着的话)
|
||||
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: `idiom-result-live-${round_id}-${Date.now()}`,
|
||||
room_id: currentRoomId || window.chatContext?.roomId || 0,
|
||||
from_user: "星海小博士",
|
||||
to_user: "大家",
|
||||
content: `🎉 【${winner_username}】率先答对成语「${answer}」,获得 ${reward_gold} 金币、${reward_exp} 经验!`,
|
||||
is_secret: false,
|
||||
font_color: "#16a34a",
|
||||
action: "idiom_result",
|
||||
winner_username,
|
||||
idiom_answer: answer,
|
||||
idiom_result_reward_gold: reward_gold,
|
||||
idiom_result_reward_exp: reward_exp,
|
||||
idiom_game_round_ended_id: round_id,
|
||||
sent_at: timeStr,
|
||||
});
|
||||
|
||||
// ── Toast 通知(所有用户都能看到) ──
|
||||
window.chatToast?.show({
|
||||
title: "🧩 猜成语",
|
||||
message: `<b>${winner_username}</b> 答对了「${answer}」,获得 ${reward_gold}💰 + ${reward_exp}⭐!`,
|
||||
icon: "🎉",
|
||||
color: "#16a34a",
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开答题弹窗。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// ── 猜成语结果消息中的用户名可点击 → 打开用户名片
|
||||
// 注:单击/双击已由 right-panel.js 的全局 [data-chat-message-user] 事件委托统一处理
|
||||
|
||||
window.setTimeout(() => {
|
||||
syncCurrentIdiomRound();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ── 挂载到 window ──
|
||||
window.openIdiomAnswerModal = openIdiomAnswerModal;
|
||||
window.closeIdiomAnswerModal = closeIdiomAnswerModal;
|
||||
window.submitIdiomAnswer = submitIdiomAnswer;
|
||||
window.handleIdiomGameStarted = handleIdiomGameStarted;
|
||||
window.handleIdiomGameAnswered = handleIdiomGameAnswered;
|
||||
@@ -2,7 +2,13 @@
|
||||
// 从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。
|
||||
|
||||
import { escapeHtml, normalizeSafeChatUrl } from "./html.js";
|
||||
import { attachIdiomAnswerButton, removeIdiomAnswerButtons } from "./idiom-quiz.js";
|
||||
import {
|
||||
attachIdiomAnswerButton,
|
||||
buildQuizActivityTitle,
|
||||
disableIdiomAnswerButtons,
|
||||
isQuizStartMessage,
|
||||
normalizeQuizRoundPayload,
|
||||
} from "./riddle-quiz.js";
|
||||
import { isExpiredChatImageMessage } from "./message-utils.js";
|
||||
import { normalizeDailyStatus, resolveBlockedSystemSenderKey } from "./preferences-status.js";
|
||||
import { escapePresenceText } from "./vip-presence.js";
|
||||
@@ -50,6 +56,80 @@ function parseBracketUsers(content, color = "#000099") {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建统一的猜谜活动标题与题型标签。
|
||||
*/
|
||||
function buildQuizBadgeHtml(msg, accentColor = "#7c3aed") {
|
||||
const { activityLabel, typeLabel } = buildQuizActivityTitle(msg);
|
||||
|
||||
return `
|
||||
<span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||
<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor};color:#fff;font-size:11px;font-weight:800;letter-spacing:.04em;">${escapeHtml(activityLabel)}</span>
|
||||
<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor}1A;color:${accentColor};font-size:11px;font-weight:700;border:1px solid ${accentColor}33;">${escapeHtml(typeLabel)}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 猜谜活动开题消息统一渲染为卡片。
|
||||
*/
|
||||
function buildQuizStartHtml(msg, timeStr) {
|
||||
const quizMeta = normalizeQuizRoundPayload(msg);
|
||||
const rawHint = String(quizMeta.hint || msg.content || "")
|
||||
.replace(/^🧩\s*/, "")
|
||||
.replace(/^📣\s*/, "")
|
||||
.replace(/^【[^】]+】\s*第\s*#?\d+\s*题开始!?\s*题面:\s*/u, "")
|
||||
.replace(/^【[^】]+】\s*/u, "")
|
||||
.replace(/^第\s*#?\d+\s*题开始!?\s*题面:\s*/u, "")
|
||||
.replace(/^题面:\s*/u, "")
|
||||
.trim();
|
||||
const safeHint = escapeHtml(rawHint);
|
||||
|
||||
return `
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:12px;background:linear-gradient(135deg,#f5f3ff,#faf5ff);border:1px solid rgba(124,58,237,.16);box-shadow:0 6px 16px rgba(124,58,237,.08);overflow:hidden;">
|
||||
<div style="width:30px;height:30px;border-radius:10px;background:linear-gradient(135deg,#7c3aed,#a78bfa);display:flex;align-items:center;justify-content:center;color:#fff;font-size:17px;box-shadow:0 5px 12px rgba(124,58,237,.18);flex-shrink:0;">🧩</div>
|
||||
<div style="min-width:0;flex:1;display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:#312e81;">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex-shrink:0;">${buildQuizBadgeHtml(msg)}</div>
|
||||
<div data-quiz-inline-text style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:13px;line-height:1.35;font-weight:700;min-width:220px;flex:1;">
|
||||
<span>${safeHint}</span>
|
||||
<span class="msg-time" style="font-size:11px;color:#94a3b8;">(${timeStr})</span>
|
||||
<span data-quiz-inline-action-anchor></span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;color:#6d28d9;font-size:11px;flex-shrink:0;margin-left:auto;">
|
||||
<span style="padding:1px 7px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">💰 ${quizMeta.rewardGold} 金币</span>
|
||||
<span style="padding:1px 7px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">⭐ ${quizMeta.rewardExp} 经验</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 猜谜活动结算消息统一渲染为结果卡片。
|
||||
*/
|
||||
function buildQuizResultHtml(msg, timeStr) {
|
||||
const quizMeta = normalizeQuizRoundPayload(msg);
|
||||
const winnerHtml = clickableUser(String(msg.winner_username || ""), "#15803d");
|
||||
const answerText = escapeHtml(quizMeta.answer || String(msg.content || ""));
|
||||
|
||||
return `
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;padding:10px 12px;border-radius:14px;background:linear-gradient(135deg,#f0fdf4,#ecfccb);border:1px solid rgba(22,163,74,.18);box-shadow:0 10px 24px rgba(34,197,94,.10);">
|
||||
<div style="width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,#16a34a,#4ade80);display:flex;align-items:center;justify-content:center;color:#fff;font-size:22px;box-shadow:0 8px 18px rgba(22,163,74,.22);flex-shrink:0;">🎉</div>
|
||||
<div style="min-width:0;flex:1;">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
${buildQuizBadgeHtml(msg, "#16a34a")}
|
||||
<span class="msg-time">(${timeStr})</span>
|
||||
</div>
|
||||
<div style="margin-top:8px;font-size:15px;line-height:1.75;color:#166534;font-weight:700;">【${winnerHtml}】率先答对「${answerText}」</div>
|
||||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:#166534;font-size:12px;">
|
||||
<span style="padding:2px 8px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(22,163,74,.12);">💰 ${quizMeta.rewardGold} 金币</span>
|
||||
<span style="padding:2px 8px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(22,163,74,.12);">⭐ ${quizMeta.rewardExp} 经验</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只保留包厢窗口最近几条猜成语答题记录,避免答题历史无限堆积。
|
||||
*/
|
||||
@@ -121,14 +201,10 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
|
||||
state.trackMaxMsgId(msg.id || 0);
|
||||
|
||||
const idiomRoundId = Number.parseInt(
|
||||
String(msg.idiom_game_round_id || msg.idom_game_round_id || "0"),
|
||||
10,
|
||||
);
|
||||
const isIdiomStartMessage = idiomRoundId > 0
|
||||
&& msg.from_user === "星海小博士"
|
||||
&& !msg.action
|
||||
&& String(msg.content || "").includes("猜成语时间");
|
||||
const quizMeta = normalizeQuizRoundPayload(msg);
|
||||
const idiomRoundId = quizMeta.roundId;
|
||||
const isIdiomStartMessage = isQuizStartMessage(msg)
|
||||
&& ["星海小博士", "系统传音"].includes(String(msg.from_user || ""));
|
||||
|
||||
if (isIdiomStartMessage) {
|
||||
const existingIdiomNode = document.querySelector(`[data-idiom-round-id="${idiomRoundId}"]`);
|
||||
@@ -150,6 +226,7 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
}
|
||||
if (idiomRoundId > 0) {
|
||||
div.dataset.idiomRoundId = String(idiomRoundId);
|
||||
div.dataset.quizRoundId = String(idiomRoundId);
|
||||
}
|
||||
if (blockRuleKey) {
|
||||
div.dataset.blockKey = blockRuleKey;
|
||||
@@ -210,12 +287,13 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
html = `${iconImg} ${parsedContent}`;
|
||||
} else if (msg.action === "idiom_result") {
|
||||
div.dataset.idiomResult = "1";
|
||||
const winnerUsername = String(msg.winner_username || "");
|
||||
const winnerHtml = clickableUser(winnerUsername, "#16a34a");
|
||||
const answerText = escapeHtml(String(msg.idiom_answer || ""));
|
||||
const rewardGold = Number.parseInt(String(msg.idiom_result_reward_gold ?? msg.reward_gold ?? 0), 10);
|
||||
const rewardExp = Number.parseInt(String(msg.idiom_result_reward_exp ?? msg.reward_exp ?? 0), 10);
|
||||
html = `<span style="color:#16a34a;font-weight:bold;">🎉 【${winnerHtml}】率先答对成语「${answerText}」,获得 ${rewardGold} 金币、${rewardExp} 经验!</span>`;
|
||||
div.dataset.quizRoundEndedId = String(quizMeta.endedRoundId || quizMeta.roundId || 0);
|
||||
div.dataset.quizWinnerUsername = String(msg.winner_username || "");
|
||||
const parsedContent = parseBracketUsers(msg.content);
|
||||
html = `${headImg}<span style="font-weight: bold;">${clickableUser(msg.from_user, fontColor, nameClass)}:</span><span class="msg-content${textColorClass}" style="color: ${fontColor};">${parsedContent}</span>`;
|
||||
} else if (isIdiomStartMessage) {
|
||||
html = buildQuizStartHtml(msg, timeStr);
|
||||
timeStrOverride = true;
|
||||
} else if (msg.action === "vip_presence") {
|
||||
const accent = msg.presence_color || "#f59e0b";
|
||||
div.style.cssText =
|
||||
@@ -278,6 +356,7 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
const isRedPacketClaimNotification = content.includes("抢到了") && content.includes("礼包");
|
||||
const isBaccaratLossCoverNotification = content.includes("【你玩游戏我买单】") || content.includes("金币补偿");
|
||||
const isDailySignInNotification = content.includes("完成今日签到") || content.includes("使用补签卡补签");
|
||||
const isQuizStartNotification = isIdiomStartMessage || content.includes("猜谜活动") || content.includes("猜成语时间");
|
||||
const isPlainNotification =
|
||||
content.includes("【百家乐】") ||
|
||||
content.includes("【赛马】") ||
|
||||
@@ -287,7 +366,23 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
content.includes("【老虎机】") ||
|
||||
content.includes("购买了");
|
||||
|
||||
if (isRedPacketClaimNotification || isBaccaratLossCoverNotification || isDailySignInNotification) {
|
||||
if (isQuizStartNotification) {
|
||||
div.style.cssText =
|
||||
"background:linear-gradient(135deg,#fff7ed,#fffbeb);border:1px solid rgba(245,158,11,.28);border-left:4px solid #f59e0b;border-radius:12px;padding:8px 12px;margin:4px 0;box-shadow:0 10px 24px rgba(245,158,11,.14);";
|
||||
html = `
|
||||
<div style="display:flex;align-items:flex-start;gap:10px;">
|
||||
<div style="width:38px;height:38px;border-radius:12px;background:linear-gradient(135deg,#f59e0b,#f97316);display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;box-shadow:0 8px 18px rgba(249,115,22,.22);flex-shrink:0;">📣</div>
|
||||
<div style="min-width:0;flex:1;">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
${buildQuizBadgeHtml(msg, "#d97706")}
|
||||
<span class="msg-time">(${timeStr})</span>
|
||||
</div>
|
||||
<div style="margin-top:7px;color:#9a3412;font-size:15px;font-weight:800;line-height:1.75;">${parseBracketUsers(content, "#b45309")}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
timeStrOverride = true;
|
||||
} else if (isRedPacketClaimNotification || isBaccaratLossCoverNotification || isDailySignInNotification) {
|
||||
let plainAccentContent = parseBracketUsers(msg.content);
|
||||
html = `<span style="color: #b45309;">🌟 ${plainAccentContent}</span>`;
|
||||
} else if (isPlainNotification) {
|
||||
@@ -346,9 +441,9 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
div.innerHTML = html;
|
||||
attachIdiomAnswerButton(div, msg);
|
||||
|
||||
// 历史消息恢复或实时结算时,都立即移除对应回合的旧答题按钮。
|
||||
if (Number.parseInt(String(msg.idiom_game_round_ended_id || "0"), 10) > 0) {
|
||||
removeIdiomAnswerButtons(Number.parseInt(String(msg.idiom_game_round_ended_id), 10));
|
||||
// 历史消息恢复或实时结算时,都立即把对应回合按钮置为结束态,保留消息结构便于回看。
|
||||
if (quizMeta.endedRoundId > 0) {
|
||||
disableIdiomAnswerButtons(quizMeta.endedRoundId, "本回合已结束", String(msg.winner_username || ""));
|
||||
}
|
||||
|
||||
// 命中屏蔽规则时,消息仍保留在 DOM 中,便于取消屏蔽后立即恢复显示。
|
||||
|
||||
@@ -478,12 +478,21 @@ export function resolveBlockedSystemSenderKey(msg) {
|
||||
const content = String(msg?.content || "");
|
||||
const action = String(msg?.action || "");
|
||||
const idiomRoundId = Number.parseInt(
|
||||
String(msg?.idiom_game_round_id || msg?.idom_game_round_id || msg?.idiom_game_round_ended_id || "0"),
|
||||
String(msg?.quiz_round_id || msg?.idiom_game_round_id || msg?.idom_game_round_id || msg?.quiz_round_ended_id || msg?.idiom_game_round_ended_id || "0"),
|
||||
10,
|
||||
);
|
||||
const quizType = String(msg?.quiz_type || "");
|
||||
const quizTypeLabel = String(msg?.quiz_type_label || "");
|
||||
|
||||
// 猜成语消息独立作为一个通知类型管理,不再复用“星海小博士”的屏蔽规则。
|
||||
if (idiomRoundId > 0 || action === "idiom_result" || (fromUser === "星海小博士" && content.includes("猜成语"))) {
|
||||
// 猜谜活动消息独立作为一个通知类型管理,不再复用“星海小博士”的屏蔽规则。
|
||||
if (
|
||||
idiomRoundId > 0 ||
|
||||
action === "idiom_result" ||
|
||||
quizType === "idiom" ||
|
||||
quizTypeLabel.includes("成语") ||
|
||||
(fromUser === "星海小博士" && (content.includes("猜成语") || content.includes("猜谜活动"))) ||
|
||||
((fromUser === "系统传音" || fromUser === "系统") && (content.includes("猜成语") || content.includes("猜谜活动")))
|
||||
) {
|
||||
return "猜成语";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
// 猜谜活动前端模块
|
||||
// 监听 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"];
|
||||
|
||||
/**
|
||||
* 兼容新旧字段,提取前端统一使用的猜谜活动回合信息。
|
||||
*/
|
||||
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 =
|
||||
"padding:4px 12px;background:linear-gradient(135deg,#7c3aed,#a78bfa);" +
|
||||
"color:#fff;border:none;border-radius:999px;font-size:11px;cursor:pointer;" +
|
||||
"font-weight:700;line-height:1.2;vertical-align:middle;box-shadow:0 4px 10px rgba(124,58,237,.18);";
|
||||
|
||||
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:11px;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.cursor = "not-allowed";
|
||||
button.style.boxShadow = "none";
|
||||
button.style.opacity = ".92";
|
||||
button.style.padding = "4px 12px";
|
||||
button.style.fontSize = "11px";
|
||||
button.style.lineHeight = "1.2";
|
||||
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.cursor = "pointer";
|
||||
button.style.boxShadow = "0 4px 10px rgba(124,58,237,.18)";
|
||||
button.style.opacity = "1";
|
||||
button.style.padding = "4px 12px";
|
||||
button.style.fontSize = "11px";
|
||||
button.style.lineHeight = "1.2";
|
||||
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: `<b>${winnerUsername}</b> 答对了「${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;
|
||||
@@ -277,12 +277,12 @@ export function initChat(roomId) {
|
||||
window.dispatchEvent(new CustomEvent("chat:pat", { detail: e }));
|
||||
})
|
||||
// 监听猜成语出题
|
||||
.listen("IdiomGameStarted", (e) => {
|
||||
.listen("RiddleGameStarted", (e) => {
|
||||
console.log("猜成语:", e);
|
||||
window.dispatchEvent(new CustomEvent("chat:idiom-started", { detail: e }));
|
||||
})
|
||||
// 监听猜成语答题结果
|
||||
.listen("IdiomGameAnswered", (e) => {
|
||||
.listen("RiddleGameAnswered", (e) => {
|
||||
console.log("猜成语结果:", e);
|
||||
window.dispatchEvent(new CustomEvent("chat:idiom-answered", { detail: e }));
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user