完善猜成语过期与答题记录逻辑
This commit is contained in:
@@ -366,41 +366,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,115 @@ function csrf() {
|
||||
let currentRoundId = 0;
|
||||
let currentRoomId = 0;
|
||||
|
||||
/**
|
||||
* 为指定回合创建统一样式的答题按钮。
|
||||
*/
|
||||
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) {
|
||||
// 当前回合同步失败时不打断聊天主流程,保留现有按钮状态。
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到猜成语出题事件时,在聊天窗口显示提示消息。
|
||||
*/
|
||||
@@ -31,6 +140,7 @@ function handleIdiomGameAnswered(e) {
|
||||
if (!answer) return;
|
||||
|
||||
currentRoundId = 0;
|
||||
removeIdiomAnswerButtons(round_id);
|
||||
|
||||
// 关闭当前用户的答题弹窗(如果开着的话)
|
||||
const answerModal = document.getElementById("idiom-answer-modal");
|
||||
@@ -38,35 +148,25 @@ function handleIdiomGameAnswered(e) {
|
||||
answerModal.style.display = "none";
|
||||
}
|
||||
|
||||
// ── 分屏文字提示 ──
|
||||
// 回答者 → 包厢(chat-messages-container2)
|
||||
// 其他人 → 公屏(chat-messages-container)
|
||||
// 实时答题结果与刷新后的历史恢复统一走 appendMessage,避免两套分流逻辑跑偏。
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, "0") + ":" +
|
||||
now.getMinutes().toString().padStart(2, "0") + ":" +
|
||||
now.getSeconds().toString().padStart(2, "0");
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "msg-line";
|
||||
|
||||
div.innerHTML = `<span style="color:#16a34a;font-weight:bold;">🎉 恭喜 <span class="msg-user" data-chat-message-user data-u="${winner_username}" style="color:#16a34a;cursor:pointer;border-bottom:1px dashed #16a34a;">${winner_username}</span> 率先答对成语「${answer}」,获得 ${reward_gold} 金币、${reward_exp} 经验!</span><span class="msg-time">(${timeStr})</span>`;
|
||||
|
||||
const isWinner = winner_username === (window.chatContext?.username || "");
|
||||
if (isWinner) {
|
||||
// 回答者 → 包厢
|
||||
const say2 = document.getElementById("chat-messages-container2");
|
||||
if (say2) {
|
||||
say2.appendChild(div.cloneNode(true));
|
||||
say2.scrollTop = say2.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
// 其他人 → 公屏
|
||||
const say1 = document.getElementById("chat-messages-container");
|
||||
if (say1) {
|
||||
say1.appendChild(div);
|
||||
say1.scrollTop = say1.scrollHeight;
|
||||
}
|
||||
}
|
||||
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({
|
||||
@@ -76,15 +176,6 @@ function handleIdiomGameAnswered(e) {
|
||||
color: "#16a34a",
|
||||
duration: 6000,
|
||||
});
|
||||
|
||||
// ── 标记所有对应 round_id 的【答题】按钮为已答 ──
|
||||
document.querySelectorAll(`[data-idiom-answer-btn="${round_id}"]`).forEach((btn) => {
|
||||
btn.dataset.idiomAnswered = "1";
|
||||
btn.textContent = "✅ 已答";
|
||||
btn.style.background = "#9ca3af";
|
||||
btn.style.cursor = "default";
|
||||
btn.style.opacity = "0.6";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,18 +328,6 @@ export function bindIdiomQuizControls() {
|
||||
const btn = e.target.closest("[data-idiom-answer-btn]");
|
||||
if (!btn) return;
|
||||
|
||||
// 已答完的按钮不可点击
|
||||
if (btn.dataset.idiomAnswered === "1") {
|
||||
window.chatToast?.show({
|
||||
title: "🧩 猜成语",
|
||||
message: "这道题已被答过了,等下一题吧!",
|
||||
icon: "😅",
|
||||
color: "#9ca3af",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roundId = parseInt(btn.dataset.idiomAnswerBtn || "0", 10);
|
||||
const hint = btn.dataset.idiomHint || "";
|
||||
const rewardGold = parseInt(btn.dataset.idiomGold || "0", 10);
|
||||
@@ -261,7 +340,10 @@ export function bindIdiomQuizControls() {
|
||||
|
||||
// ── 猜成语结果消息中的用户名可点击 → 打开用户名片
|
||||
// 注:单击/双击已由 right-panel.js 的全局 [data-chat-message-user] 事件委托统一处理
|
||||
|
||||
|
||||
window.setTimeout(() => {
|
||||
syncCurrentIdiomRound();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ── 挂载到 window ──
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。
|
||||
|
||||
import { escapeHtml, normalizeSafeChatUrl } from "./html.js";
|
||||
import { attachIdiomAnswerButton, removeIdiomAnswerButtons } from "./idiom-quiz.js";
|
||||
import { isExpiredChatImageMessage } from "./message-utils.js";
|
||||
import { normalizeDailyStatus, resolveBlockedSystemSenderKey } from "./preferences-status.js";
|
||||
import { escapePresenceText } from "./vip-presence.js";
|
||||
@@ -49,6 +50,21 @@ function parseBracketUsers(content, color = "#000099") {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 只保留包厢窗口最近几条猜成语答题记录,避免答题历史无限堆积。
|
||||
*/
|
||||
function prunePrivateIdiomResultMessages(targetContainer, maxRecords = 3) {
|
||||
if (!targetContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = Array.from(targetContainer.querySelectorAll('[data-idiom-result="1"]'));
|
||||
while (nodes.length > maxRecords) {
|
||||
const firstNode = nodes.shift();
|
||||
firstNode?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建聊天消息的内容 HTML。
|
||||
*/
|
||||
@@ -172,6 +188,14 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
const iconImg = `<img src="/images/bugle.png" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;mix-blend-mode: multiply;" onerror="this.src='/images/headface/1.gif'">`;
|
||||
const parsedContent = parseBracketUsers(msg.content);
|
||||
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>`;
|
||||
} else if (msg.action === "vip_presence") {
|
||||
const accent = msg.presence_color || "#f59e0b";
|
||||
div.style.cssText =
|
||||
@@ -300,6 +324,12 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
html += ` <span class="msg-time">(${timeStr})</span>`;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// 命中屏蔽规则时,消息仍保留在 DOM 中,便于取消屏蔽后立即恢复显示。
|
||||
if (shouldHideByBlock) {
|
||||
@@ -325,7 +355,8 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
}
|
||||
|
||||
// 路由规则:公众窗口(say1) — 别人的公聊消息;包厢窗口(say2) — 自己发的 + 悄悄话 + 对自己说的
|
||||
const isRelatedToMe = isMe || msg.is_secret || msg.to_user === window.chatContext?.username;
|
||||
const isIdiomWinnerHistory = msg.action === "idiom_result" && msg.winner_username === window.chatContext?.username;
|
||||
const isRelatedToMe = isMe || msg.is_secret || msg.to_user === window.chatContext?.username || isIdiomWinnerHistory;
|
||||
|
||||
// 存点通知标记
|
||||
const isAutoSave = (msg.from_user === "系统" || msg.from_user === "") &&
|
||||
@@ -343,12 +374,18 @@ export function appendMessage(msg, renderBatch = null) {
|
||||
renderBatch.privateFragment.appendChild(div);
|
||||
renderBatch.shouldPrunePrivate = true;
|
||||
renderBatch.shouldScrollPrivate = renderBatch.shouldScrollPrivate || state.autoScroll;
|
||||
if (msg.action === "idiom_result") {
|
||||
renderBatch.shouldPrunePrivateIdiomResults = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const container2 = state.container2;
|
||||
if (container2) {
|
||||
container2.appendChild(div);
|
||||
pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
|
||||
if (msg.action === "idiom_result") {
|
||||
prunePrivateIdiomResultMessages(container2, 3);
|
||||
}
|
||||
if (state.autoScroll) {
|
||||
container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
@@ -398,6 +435,7 @@ export function createChatMessageRenderBatch() {
|
||||
privateFragment: document.createDocumentFragment(),
|
||||
shouldPrunePublic: false,
|
||||
shouldPrunePrivate: false,
|
||||
shouldPrunePrivateIdiomResults: false,
|
||||
shouldScrollPublic: false,
|
||||
shouldScrollPrivate: false,
|
||||
};
|
||||
@@ -429,6 +467,10 @@ export function commitChatMessageRenderBatch(renderBatch) {
|
||||
const container2 = state.container2;
|
||||
if (container2) pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
|
||||
}
|
||||
if (renderBatch.shouldPrunePrivateIdiomResults) {
|
||||
const container2 = state.container2;
|
||||
if (container2) prunePrivateIdiomResultMessages(container2, 3);
|
||||
}
|
||||
if (renderBatch.shouldScrollPublic) {
|
||||
const container = state.container;
|
||||
if (container) container.scrollTop = container.scrollHeight;
|
||||
|
||||
Reference in New Issue
Block a user