1850a5f4e9
- 统一在 selectCommand 公共入口清理输入 - 新增 /签到 命令,自动完成今日签到
281 lines
8.1 KiB
JavaScript
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 };
|