fix: 修复迁移遗留的按钮无响应、头像框层级及构建错误
迁移收尾修复:
- heartbeat.js: 移除 export { } 中重复的 startHeartbeat/stopHeartbeat(已通过 export function 导出)
- scripts.blade.php: 移除 JS 注释中的 {{ }} 避免 Blade 编译为 e() 导致 PHP 解析错误
- preferences-status.js: 补全 6 个缺失的 window.* 赋值(toggleBlockMenu/toggleFeatureMenu 等),
实现迁移中丢失的 updateDailyStatus/clearDailyStatus,修复 handleFeatureLocalClear 清屏回调
- toolbar.js: 补全 window.runFeatureShortcut 赋值
头像框样式修复(chat-decorations.css):
- z-index 互换:头像降至 1,框升至 3,使框边缘可遮挡头像外围
- 使用 CSS mask(radial-gradient)挖环形替代旧 ::before 实心圆遮挡方案
- clip-path: circle(50%) 硬裁剪确保圆形,不受 chat.css border-radius: 2px 覆盖
- 特异性提升至 .user-item .avatar-frame-wrapper .user-head
新 Vite 模块(从 Blade 迁移):
- chat-state.js / message-renderer.js / user-list.js / chat-events.js
- composer.js(重写)/ heartbeat.js / admin-commands.js
- vip-presence.js / chat-decorations.css
This commit is contained in:
@@ -1,40 +1,347 @@
|
||||
// 聊天输入区事件绑定,逐步替代底部输入栏内联提交事件。
|
||||
// 聊天输入区完整逻辑:发送消息、草稿管理、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 "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── 消息发送 ──
|
||||
|
||||
/**
|
||||
* 绑定聊天表单提交事件。
|
||||
* 发送主流程仍由 Blade 主脚本的 sendMessage 维护,这里只统一 submit 入口。
|
||||
*
|
||||
* @returns {void}
|
||||
* 将当前输入区状态整理为一份稳定快照。
|
||||
*/
|
||||
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 && !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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置聊天动作并把焦点带回输入框。
|
||||
* 该入口兼容旧模板可能存在的 `setAction(...)` 调用,切换右侧标签仍交给 Blade 里的 switchTab 处理。
|
||||
*
|
||||
* @param {string} action 动作名称
|
||||
* @param {(tab:string) => void} switchTabHandler 右侧标签切换函数
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setChatComposerAction(
|
||||
action,
|
||||
@@ -47,7 +354,6 @@ export function setChatComposerAction(
|
||||
actionSelect.value = action;
|
||||
}
|
||||
|
||||
// 右侧在线名单切换仍在 Blade 主脚本内,模块只通过兼容入口调用。
|
||||
if (typeof switchTabHandler === "function") {
|
||||
switchTabHandler("users");
|
||||
}
|
||||
@@ -56,3 +362,15 @@ export function setChatComposerAction(
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user