Files
chatroom/resources/js/chat-room/slash-commands.js
T
pllx 1850a5f4e9 fix: 选择斜杠命令后自动清除输入框中的 /
- 统一在 selectCommand 公共入口清理输入
- 新增 /签到 命令,自动完成今日签到
2026-04-28 23:12:30 +08:00

281 lines
8.1 KiB
JavaScript

// 斜杠命令菜单模块
// 输入 / 时弹出可用命令列表,支持键盘/鼠标选择,可扩展的命令注册表。
// ── 命令注册表(后续新命令只需 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
? `<span style="font-size:11px;color:#64748b;margin-left:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${cmd.description}</span>`
: "";
item.innerHTML =
`<span style="font-size:16px;flex:none;">${cmd.icon}</span>` +
`<span style="flex:1;min-width:0;display:flex;align-items:baseline;gap:4px;">${nameHtml}${descHtml}</span>`;
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) +
`<b style="color:#4f46e5;">${text.slice(idx, idx + filter.length)}</b>` +
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 };