Files
chatroom/resources/js/chat-room/composer.js
T
pllx 495efdf9e0 feat: 新增 /拍一拍 功能 + 斜杠命令菜单
- 输入框输入 / 弹出命令菜单,当前支持 /拍一拍
- 选择对象后输入 /拍一拍 发送拍一拍通知
- 所有在线用户屏幕抖动 + 正常聊天样式显示消息
- 命令注册表可扩展,后续新增命令只需 push 到数组
2026-04-28 22:59:16 +08:00

389 lines
12 KiB
JavaScript

// 聊天输入区完整逻辑:发送消息、草稿管理、IME 防重、神秘箱子暗号拦截。
// 从 Blade 内联脚本 scripts.blade.php 迁移至 Vite 模块。
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);
say2.scrollTop = say2.scrollHeight;
}
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 };