// 斜杠命令菜单模块 // 输入 / 时弹出可用命令列表,支持键盘/鼠标选择,可扩展的命令注册表。 // ── 命令注册表(后续新命令只需 push 到此数组)── const SLASH_COMMANDS = [ { id: "pat", name: "/拍一拍", description: "向当前选中的聊天对象发送拍一拍,屏幕会抖动", icon: "👋", fill(_input) { if (typeof window.executePat === "function") { window.executePat(); } }, }, { id: "profile", name: "/查看资料", description: "查看当前选中对象的个人资料名片", icon: "📋", fill(_input) { const toUserSelect = document.getElementById("to_user"); const target = toUserSelect?.value?.trim() || null; if (!target || target === "大家") { window.chatDialog?.alert("请先选择一个聊天对象,再查看资料。", "查看资料", "#6366f1"); return; } if (typeof window.openUserCard === "function") { window.openUserCard(target); } }, }, { id: "signin", name: "/签到", description: "自动完成今日签到,领取每日奖励", icon: "✅", fill(_input) { if (typeof window.claimDailySignInFromModal === "function") { window.claimDailySignInFromModal(); } }, }, ]; // ── 菜单状态 ── let visible = false; let selectedIndex = 0; let currentFilter = ""; // ── 过滤 ── function getFilteredCommands(filter) { if (!filter || filter === "/") return SLASH_COMMANDS; const q = filter.toLowerCase(); return SLASH_COMMANDS.filter((c) => c.id.includes(q) || c.name.includes(q)); } // ── DOM 构建 ── function ensureMenu() { const input = document.getElementById("content"); const inputRow = input?.closest(".input-row"); if (!input || !inputRow) return null; let menu = document.getElementById("slash-command-menu"); if (!menu) { menu = document.createElement("div"); menu.id = "slash-command-menu"; menu.className = "slash-command-menu"; menu.style.cssText = "display:none;position:absolute;bottom:100%;left:0;z-index:9999;" + "min-width:380px;max-width:420px;max-height:200px;overflow-y:auto;" + "background:#fff;border:1px solid #cbd5e1;border-radius:10px;" + "box-shadow:0 6px 20px rgba(15,23,42,.18);padding:6px 0;"; inputRow.style.position = "relative"; inputRow.appendChild(menu); } return menu; } function renderMenu(filtered, filter) { const menu = ensureMenu(); if (!menu) return; menu.innerHTML = ""; if (filtered.length === 0) { const empty = document.createElement("div"); empty.style.cssText = "padding:10px 14px;color:#94a3b8;font-size:12px;text-align:center;"; empty.textContent = "没有匹配的命令"; menu.appendChild(empty); return; } filtered.forEach((cmd, i) => { const item = document.createElement("div"); item.dataset.index = String(i); item.style.cssText = "display:flex;align-items:center;gap:10px;padding:8px 14px;" + "cursor:pointer;transition:background .1s;white-space:nowrap;" + (i === selectedIndex ? "background:#eef2ff;" : ""); // 高亮匹配文字 const nameHtml = highlightMatch(cmd.name, filter); const descHtml = cmd.description ? `${cmd.description}` : ""; item.innerHTML = `${cmd.icon}` + `${nameHtml}${descHtml}`; item.addEventListener("mousedown", (e) => { e.preventDefault(); selectCommand(i); }); item.addEventListener("mouseenter", () => { selectedIndex = i; highlightItem(menu, i); }); menu.appendChild(item); }); } function highlightMatch(text, filter) { if (!filter || filter === "/") return text; const idx = text.toLowerCase().indexOf(filter.toLowerCase()); if (idx === -1) return text; return ( text.slice(0, idx) + `${text.slice(idx, idx + filter.length)}` + text.slice(idx + filter.length) ); } function highlightItem(menu, index) { const items = menu.querySelectorAll("[data-index]"); items.forEach((el, i) => { el.style.background = i === index ? "#eef2ff" : ""; }); } // ── 选择 ── function selectCommand(index) { const filtered = getFilteredCommands(currentFilter); const cmd = filtered[index]; if (!cmd) return; const input = document.getElementById("content"); if (!input) return; if (typeof cmd.fill === "function") { cmd.fill(input); } else { input.value = cmd.name; window.persistChatDraft?.(cmd.name); } // 统一清除输入框中的 / input.value = ""; window.persistChatDraft?.(""); hideMenu(); input.focus(); } // ── 显示/隐藏 ── function showMenu(filter) { const menu = ensureMenu(); if (!menu) return; currentFilter = filter; selectedIndex = 0; const filtered = getFilteredCommands(filter); visible = filtered.length > 0; renderMenu(filtered, filter); menu.style.display = visible ? "block" : "none"; } function hideMenu() { const menu = document.getElementById("slash-command-menu"); if (menu) menu.style.display = "none"; visible = false; selectedIndex = 0; currentFilter = ""; } // ── 事件绑定 ── function handleInput(e) { const input = e.target; const val = input.value; if (val.startsWith("/")) { // 如果输入值已是完整命令名,不弹出菜单 const exactMatch = SLASH_COMMANDS.some( (c) => c.name === val.trim() ); if (!exactMatch) { const filter = val.trim(); showMenu(filter); return; } } hideMenu(); } function handleKeydown(e) { if (!visible) return; const filtered = getFilteredCommands(currentFilter); if (filtered.length === 0) return; switch (e.key) { case "ArrowDown": e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, filtered.length - 1); highlightItem(ensureMenu(), selectedIndex); break; case "ArrowUp": e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, 0); highlightItem(ensureMenu(), selectedIndex); break; case "Enter": if (visible) { e.preventDefault(); selectCommand(selectedIndex); } break; case "Escape": e.preventDefault(); hideMenu(); break; } } function handleDocumentClick(e) { if (visible) { const menu = document.getElementById("slash-command-menu"); const input = document.getElementById("content"); if (menu && !menu.contains(e.target) && input !== e.target) { hideMenu(); } } } // ── 初始化 ── export function bindSlashCommands() { const input = document.getElementById("content"); if (!input) { // 如果 DOM 未就绪,稍后重试 setTimeout(bindSlashCommands, 200); return; } // 避免重复绑定 if (input.dataset.slashBound) return; input.dataset.slashBound = "1"; input.addEventListener("input", handleInput); input.addEventListener("keydown", handleKeydown); document.addEventListener("click", handleDocumentClick); } // 允许外部扩展命令列表 export function registerSlashCommand(cmd) { SLASH_COMMANDS.push(cmd); } export { SLASH_COMMANDS };