391 lines
12 KiB
JavaScript
391 lines
12 KiB
JavaScript
// 聊天输入区完整逻辑:发送消息、草稿管理、IME 防重、神秘箱子暗号拦截。
|
|
// 从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。
|
|
|
|
import { isAutoScrollEnabled, scrollChatToBottom } from "./message-utils.js";
|
|
|
|
let chatComposerEventsBound = false;
|
|
|
|
function csrf() {
|
|
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
|
}
|
|
|
|
function getState() {
|
|
return window.chatState;
|
|
}
|
|
|
|
function getDraftStorageKey() {
|
|
return `chat_draft_${window.chatContext?.roomId ?? '0'}_${window.chatContext?.userId ?? '0'}`;
|
|
}
|
|
|
|
// ── 草稿管理 ──
|
|
|
|
function persistChatDraft(value = null) {
|
|
try {
|
|
const contentInput = document.getElementById("content");
|
|
const draft = value ?? contentInput?.value ?? "";
|
|
if (draft === "") {
|
|
sessionStorage.removeItem(getDraftStorageKey());
|
|
return;
|
|
}
|
|
sessionStorage.setItem(getDraftStorageKey(), draft);
|
|
} catch (_) {
|
|
// 会话存储不可用时静默降级
|
|
}
|
|
}
|
|
|
|
function loadChatDraft() {
|
|
try {
|
|
return sessionStorage.getItem(getDraftStorageKey()) || "";
|
|
} catch (_) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// ── 消息发送 ──
|
|
|
|
/**
|
|
* 将当前输入区状态整理为一份稳定快照。
|
|
*/
|
|
function collectChatComposerState() {
|
|
const contentInput = document.getElementById("content");
|
|
const submitBtn = document.getElementById("send-btn");
|
|
const imageInput = document.getElementById("chat_image");
|
|
const toUserSelect = document.getElementById("to_user");
|
|
const actionSelect = document.getElementById("action");
|
|
const fontColorInput = document.getElementById("font_color");
|
|
const secretCheckbox = document.getElementById("is_secret");
|
|
const contentRaw = contentInput?.value ?? "";
|
|
const selectedImage = imageInput?.files?.[0] ?? null;
|
|
|
|
return {
|
|
contentInput,
|
|
submitBtn,
|
|
imageInput,
|
|
contentRaw,
|
|
content: contentRaw.trim(),
|
|
selectedImage,
|
|
toUser: toUserSelect?.value || "大家",
|
|
action: actionSelect?.value || "",
|
|
fontColor: fontColorInput?.value || "",
|
|
isSecret: Boolean(secretCheckbox?.checked),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 基于当前聊天快照构造稳定的 multipart 请求体。
|
|
*/
|
|
function buildChatMessageFormData(composerState) {
|
|
const formData = new FormData();
|
|
formData.append("content", composerState.contentRaw);
|
|
formData.append("to_user", composerState.toUser);
|
|
formData.append("action", composerState.action);
|
|
formData.append("font_color", composerState.fontColor);
|
|
|
|
if (composerState.isSecret) {
|
|
formData.append("is_secret", "1");
|
|
}
|
|
if (composerState.selectedImage) {
|
|
formData.append("image", composerState.selectedImage);
|
|
}
|
|
|
|
return formData;
|
|
}
|
|
|
|
/**
|
|
* 处理聊天图片选择后的前端状态展示。
|
|
*/
|
|
function handleChatImageSelected(input) {
|
|
const file = input?.files?.[0] ?? null;
|
|
if (!file) return;
|
|
// 用户选择图片后,立即触发自动发送
|
|
sendMessage(null);
|
|
}
|
|
|
|
/**
|
|
* 清理当前选中的聊天图片。
|
|
*/
|
|
function clearSelectedChatImage(resetInput = false) {
|
|
const imageInput = document.getElementById("chat_image");
|
|
if (resetInput && imageInput) {
|
|
imageInput.value = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 页面从后台恢复后,同步草稿、图片提示和发送锁状态。
|
|
*/
|
|
function syncChatComposerAfterResume() {
|
|
const state = getState();
|
|
const contentInput = document.getElementById("content");
|
|
if (!contentInput) return;
|
|
|
|
const savedDraft = loadChatDraft();
|
|
if (contentInput.value === "" && savedDraft !== "") {
|
|
contentInput.value = savedDraft;
|
|
} else if (contentInput.value !== "") {
|
|
persistChatDraft(contentInput.value);
|
|
}
|
|
|
|
const imageInput = document.getElementById("chat_image");
|
|
if (!imageInput?.files?.length) {
|
|
clearSelectedChatImage();
|
|
}
|
|
|
|
if (state) {
|
|
state.imeComposing = false;
|
|
}
|
|
|
|
if (state && state.isSending && Date.now() - state.sendStartedAt > 15000) {
|
|
const submitBtn = document.getElementById("send-btn");
|
|
if (submitBtn) submitBtn.disabled = false;
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送聊天消息(内带防重入锁,避免快速连按 Enter 重复提交)。
|
|
*/
|
|
async function sendMessage(e) {
|
|
if (e) e.preventDefault();
|
|
|
|
const state = getState();
|
|
if (state?.isSending) return;
|
|
|
|
if (state) {
|
|
state.isSending = true;
|
|
state.sendStartedAt = Date.now();
|
|
}
|
|
|
|
// 前端禁言检查
|
|
if (state && state.isMutedUntil > Date.now()) {
|
|
const remaining = Math.ceil((state.isMutedUntil - Date.now()) / 1000);
|
|
const remainMin = Math.ceil(remaining / 60);
|
|
const muteDiv = document.createElement("div");
|
|
muteDiv.className = "msg-line";
|
|
muteDiv.innerHTML = `<span style="color: #dc2626; font-weight: bold;">【提示】您正在禁言中,还需等待约 ${remainMin} 分钟(${remaining} 秒)后方可发言。</span>`;
|
|
const say2 = document.getElementById("say2");
|
|
if (say2) {
|
|
say2.appendChild(muteDiv);
|
|
scrollChatToBottom(say2, isAutoScrollEnabled);
|
|
}
|
|
if (state) {
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const composerState = collectChatComposerState();
|
|
const { contentInput, submitBtn, content, contentRaw, selectedImage, toUser } = composerState;
|
|
|
|
// 拦截 /拍一拍 命令:使用当前选中的聊天对象
|
|
if (content && typeof window.isPatCommand === "function" && window.isPatCommand(content)) {
|
|
if (state) {
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
if (typeof window.executePat === "function") {
|
|
await window.executePat();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!content && !selectedImage) {
|
|
contentInput?.focus();
|
|
if (state) {
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// AI 小助手私聊转发
|
|
if (toUser === "AI小班长" && content && typeof window.sendToChatBot === "function") {
|
|
window.sendToChatBot(content, composerState.isSecret);
|
|
}
|
|
|
|
// ── 神秘箱子暗号拦截 ──
|
|
const passcodePattern = /^[A-Z0-9]{4,8}$/;
|
|
if (!selectedImage && passcodePattern.test(content.trim())) {
|
|
if (state) {
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
|
|
try {
|
|
const claimRes = await fetch("/mystery-box/claim", {
|
|
method: "POST",
|
|
headers: {
|
|
"X-CSRF-TOKEN": csrf(),
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
body: JSON.stringify({ passcode: content.trim() }),
|
|
});
|
|
const claimData = await claimRes.json();
|
|
|
|
if (claimData.ok) {
|
|
contentInput.value = "";
|
|
persistChatDraft("");
|
|
contentInput.focus();
|
|
window._mysteryBoxActive = false;
|
|
window._mysteryBoxPasscode = null;
|
|
|
|
const isPositive = (claimData.reward ?? 1) >= 0;
|
|
window.chatDialog?.alert(
|
|
claimData.message || "开箱成功!",
|
|
isPositive ? "🎉 恭喜!" : "☠️ 中了陷阱!",
|
|
isPositive ? "#10b981" : "#ef4444"
|
|
);
|
|
if (window.__chatUser && claimData.balance !== undefined) {
|
|
window.__chatUser.jjb = claimData.balance;
|
|
}
|
|
return;
|
|
}
|
|
} catch (_) {
|
|
// 网络错误时静默回退正常发送
|
|
}
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
const formData = buildChatMessageFormData({ ...composerState, contentRaw });
|
|
|
|
try {
|
|
const response = await fetch(window.chatContext.sendUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"X-CSRF-TOKEN": csrf(),
|
|
"Accept": "application/json",
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok && data.status === "success") {
|
|
contentInput.value = "";
|
|
persistChatDraft("");
|
|
clearSelectedChatImage(true);
|
|
contentInput.focus();
|
|
} else {
|
|
window.chatDialog?.alert(
|
|
"发送失败: " + (data.message || JSON.stringify(data.errors)),
|
|
"操作失败",
|
|
"#cc4444"
|
|
);
|
|
}
|
|
} catch (error) {
|
|
window.chatDialog?.alert("网络连接错误,消息发送失败!", "网络错误", "#cc4444");
|
|
console.error(error);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
if (state) {
|
|
state.isSending = false;
|
|
state.sendStartedAt = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 事件绑定 ──
|
|
|
|
/**
|
|
* 绑定聊天输入区的所有事件:submit、IME、keydown、草稿、焦点恢复。
|
|
*/
|
|
export function bindChatComposerControls() {
|
|
if (chatComposerEventsBound || typeof document === "undefined") {
|
|
return;
|
|
}
|
|
chatComposerEventsBound = true;
|
|
|
|
// 表单提交
|
|
document.addEventListener("submit", (event) => {
|
|
const form = event.target;
|
|
if (!(form instanceof HTMLFormElement) || !form.matches("[data-chat-form]")) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
if (typeof window.sendMessage === "function") {
|
|
void window.sendMessage(event);
|
|
}
|
|
});
|
|
|
|
// 输入框事件绑定
|
|
const contentInput = document.getElementById("content");
|
|
if (contentInput) {
|
|
contentInput.addEventListener("input", function () {
|
|
persistChatDraft(this.value);
|
|
});
|
|
|
|
// IME 组词开始
|
|
contentInput.addEventListener("compositionstart", () => {
|
|
const state = getState();
|
|
if (state) state.imeComposing = true;
|
|
});
|
|
|
|
// IME 组词结束
|
|
contentInput.addEventListener("compositionend", () => {
|
|
setTimeout(() => {
|
|
const state = getState();
|
|
if (state) state.imeComposing = false;
|
|
}, 10);
|
|
});
|
|
|
|
// Enter 发送
|
|
contentInput.addEventListener("keydown", function (e) {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
const state = getState();
|
|
if (state?.imeComposing) return;
|
|
sendMessage(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 页面恢复事件
|
|
syncChatComposerAfterResume();
|
|
window.addEventListener("pageshow", syncChatComposerAfterResume);
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (document.visibilityState === "visible") {
|
|
syncChatComposerAfterResume();
|
|
}
|
|
});
|
|
window.addEventListener("focus", function () {
|
|
setTimeout(syncChatComposerAfterResume, 0);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 设置聊天动作并把焦点带回输入框。
|
|
*/
|
|
export function setChatComposerAction(
|
|
action,
|
|
switchTabHandler = typeof window !== "undefined" ? window.switchTab : null,
|
|
) {
|
|
const actionSelect = document.getElementById("action");
|
|
const contentInput = document.getElementById("content");
|
|
|
|
if (actionSelect) {
|
|
actionSelect.value = action;
|
|
}
|
|
|
|
if (typeof switchTabHandler === "function") {
|
|
switchTabHandler("users");
|
|
}
|
|
|
|
if (contentInput) {
|
|
contentInput.focus();
|
|
}
|
|
}
|
|
|
|
// ── 挂载到 window ──
|
|
window.sendMessage = sendMessage;
|
|
window.handleChatImageSelected = handleChatImageSelected;
|
|
window.collectChatComposerState = collectChatComposerState;
|
|
window.buildChatMessageFormData = buildChatMessageFormData;
|
|
window.clearSelectedChatImage = clearSelectedChatImage;
|
|
window.persistChatDraft = persistChatDraft;
|
|
window.loadChatDraft = loadChatDraft;
|
|
window.syncChatComposerAfterResume = syncChatComposerAfterResume;
|
|
|
|
export { sendMessage, handleChatImageSelected, clearSelectedChatImage, collectChatComposerState, buildChatMessageFormData };
|