feat: 新增 /拍一拍 功能 + 斜杠命令菜单
- 输入框输入 / 弹出命令菜单,当前支持 /拍一拍 - 选择对象后输入 /拍一拍 发送拍一拍通知 - 所有在线用户屏幕抖动 + 正常聊天样式显示消息 - 命令注册表可扩展,后续新增命令只需 push 到数组
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// 拍一拍功能模块
|
||||
// 拦截输入框中的 /拍一拍 命令,向所选对象发送拍一拍通知并触发屏幕抖动。
|
||||
|
||||
import { pruneMessageContainer } from "./message-renderer.js";
|
||||
|
||||
function csrf() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断输入是否为 /拍一拍 命令。
|
||||
*/
|
||||
function isPatCommand(text) {
|
||||
return /^\/拍一拍\s*$/.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的聊天对象。
|
||||
*/
|
||||
function getSelectedTarget() {
|
||||
const toUserSelect = document.getElementById("to_user");
|
||||
if (!toUserSelect) return null;
|
||||
const val = toUserSelect.value?.trim();
|
||||
return val || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行拍一拍请求。
|
||||
*/
|
||||
async function executePat() {
|
||||
const targetUser = getSelectedTarget();
|
||||
if (!targetUser || targetUser === "大家") {
|
||||
window.chatDialog?.alert("请先选择一个聊天对象(不能为大家),再进行拍一拍。", "拍一拍", "#f472b6");
|
||||
return false;
|
||||
}
|
||||
|
||||
const roomId = window.chatContext?.roomId;
|
||||
if (!roomId) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/room/${roomId}/pat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": csrf(),
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ target_user: targetUser }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
// 清空输入并触发本机抖动
|
||||
const contentInput = document.getElementById("content");
|
||||
if (contentInput) {
|
||||
contentInput.value = "";
|
||||
if (typeof window.persistChatDraft === "function") {
|
||||
window.persistChatDraft("");
|
||||
}
|
||||
contentInput.focus();
|
||||
}
|
||||
triggerPatShake();
|
||||
return true;
|
||||
}
|
||||
|
||||
window.chatDialog?.alert(data.message || "拍一拍失败", "拍一拍", "#f472b6");
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("拍一拍请求失败:", error);
|
||||
window.chatDialog?.alert("网络错误,拍一拍发送失败。", "拍一拍", "#f472b6");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发屏幕抖动动画。
|
||||
*/
|
||||
function triggerPatShake() {
|
||||
const layout = document.querySelector(".chat-layout");
|
||||
if (!layout) return;
|
||||
|
||||
layout.classList.remove("chat-shake");
|
||||
// 强制回流后重新添加动画
|
||||
void layout.offsetWidth;
|
||||
layout.classList.add("chat-shake");
|
||||
|
||||
// 动画结束后移除 class
|
||||
setTimeout(() => {
|
||||
layout.classList.remove("chat-shake");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加拍一拍消息到聊天窗口(使用正常聊天样式渲染)。
|
||||
*/
|
||||
function appendPatMessage(displayText, fromUserHeadface, fromUser, targetUser) {
|
||||
const state = window.chatState;
|
||||
const container = state?.container;
|
||||
if (!container) return;
|
||||
|
||||
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 fromUserSafe = fromUser || "";
|
||||
const targetUserSafe = targetUser || "";
|
||||
|
||||
// 获取发送者的在线数据,获取正确头像
|
||||
const senderInfo = state.onlineUsers[fromUserSafe];
|
||||
const senderHead = (senderInfo && senderInfo.headface) || "1.gif";
|
||||
let headImgSrc = senderHead.startsWith("storage/") ? "/" + senderHead : "/images/headface/" + senderHead;
|
||||
|
||||
const headImg = '<img src="' + headImgSrc + '" 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 fromHtml = '<span class="msg-user" data-chat-message-user data-u="' + fromUserSafe + '" style="color: #000099; cursor: pointer;">' + fromUserSafe + '</span>';
|
||||
const toHtml = '<span class="msg-user" data-chat-message-user data-u="' + targetUserSafe + '" style="color: #000099; cursor: pointer;">' + targetUserSafe + '</span>';
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "msg-line";
|
||||
if (fromUserSafe) {
|
||||
div.dataset.fromUser = fromUserSafe;
|
||||
}
|
||||
|
||||
div.innerHTML = headImg + fromHtml + "对" + toHtml + "说:<span class=\"msg-content\" style=\"color: #000000\">👋 我刚拍了拍你</span> <span class=\"msg-time\">(" + timeStr + ")</span>";
|
||||
|
||||
container.appendChild(div);
|
||||
pruneMessageContainer(container, 600);
|
||||
if (state?.autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 同时在包厢窗口(say2)也显示
|
||||
const container2 = state?.container2;
|
||||
if (container2) {
|
||||
const div2 = div.cloneNode(true);
|
||||
container2.appendChild(div2);
|
||||
pruneMessageContainer(container2, 300);
|
||||
if (state?.autoScroll) {
|
||||
container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 导出 ──
|
||||
export { isPatCommand, executePat, triggerPatShake, appendPatMessage };
|
||||
|
||||
// 挂载到 window 供其他模块使用
|
||||
window.isPatCommand = isPatCommand;
|
||||
window.executePat = executePat;
|
||||
window.triggerPatShake = triggerPatShake;
|
||||
window.appendPatMessage = appendPatMessage;
|
||||
Reference in New Issue
Block a user