feat: ai's attempt
This commit is contained in:
parent
6da2284ead
commit
a4023c994f
|
|
@ -5,6 +5,7 @@ import './md-link';
|
||||||
import './md-pins';
|
import './md-pins';
|
||||||
import './md-bg';
|
import './md-bg';
|
||||||
import './md-deck';
|
import './md-deck';
|
||||||
|
import './md-commander/index';
|
||||||
|
|
||||||
// 导出组件
|
// 导出组件
|
||||||
export { Article } from './Article';
|
export { Article } from './Article';
|
||||||
|
|
@ -17,3 +18,11 @@ export { FileTreeNode, HeadingNode } from './FileTree';
|
||||||
export type { DiceProps } from './md-dice';
|
export type { DiceProps } from './md-dice';
|
||||||
export type { TableProps } from './md-table';
|
export type { TableProps } from './md-table';
|
||||||
export type { BgProps } from './md-bg';
|
export type { BgProps } from './md-bg';
|
||||||
|
export type {
|
||||||
|
MdCommanderProps,
|
||||||
|
MdCommanderCommand,
|
||||||
|
MdCommanderOption,
|
||||||
|
MdCommanderOptionType,
|
||||||
|
CommanderEntry,
|
||||||
|
CompletionItem,
|
||||||
|
} from './md-commander';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { type Component, For, Show } from "solid-js";
|
||||||
|
import type { CommanderEntry } from "./types";
|
||||||
|
import { getResultClass } from "./hooks";
|
||||||
|
|
||||||
|
export interface CommanderEntriesProps {
|
||||||
|
entries: () => CommanderEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2">
|
||||||
|
<Show
|
||||||
|
when={props.entries().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="text-gray-400 text-center py-8">暂无命令执行记录</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={props.entries()}>
|
||||||
|
{(entry) => (
|
||||||
|
<div class="border-l-2 border-gray-300 pl-3 py-1">
|
||||||
|
<div class="flex items-center justify-between text-xs text-gray-500 mb-1">
|
||||||
|
<span class="font-mono">{entry.command}</span>
|
||||||
|
<span>
|
||||||
|
{entry.timestamp.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
|
||||||
|
>
|
||||||
|
{entry.result.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { type Component, Show } from "solid-js";
|
||||||
|
import type { CompletionItem } from "./types";
|
||||||
|
|
||||||
|
export interface CommanderInputProps {
|
||||||
|
placeholder?: string;
|
||||||
|
inputValue: () => string;
|
||||||
|
onInput: (e: Event) => void;
|
||||||
|
onKeyDown: (e: KeyboardEvent) => void;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
showCompletions: () => boolean;
|
||||||
|
completions: () => CompletionItem[];
|
||||||
|
selectedCompletion: () => number;
|
||||||
|
onSelectCompletion: (idx: number) => void;
|
||||||
|
onAcceptCompletion: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center border-b border-gray-300 bg-gray-50 px-3 py-2">
|
||||||
|
<span class="text-gray-500 mr-2">❯</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={props.inputValue()}
|
||||||
|
onInput={props.onInput}
|
||||||
|
onKeyDown={props.onKeyDown}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
onBlur={props.onBlur}
|
||||||
|
placeholder={props.placeholder || "输入命令..."}
|
||||||
|
class="flex-1 bg-transparent outline-none text-gray-800"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={props.onSubmit}
|
||||||
|
class="ml-2 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
执行
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 自动补全下拉框 */}
|
||||||
|
<Show when={props.showCompletions() && props.completions().length > 0}>
|
||||||
|
<div class="absolute z-10 w-full bg-white border border-gray-300 shadow-lg max-h-48 overflow-auto mt-1">
|
||||||
|
{props.completions().map((comp, idx) => (
|
||||||
|
<div
|
||||||
|
class={`px-3 py-2 cursor-pointer flex justify-between items-center ${
|
||||||
|
idx === props.selectedCompletion()
|
||||||
|
? "bg-blue-100"
|
||||||
|
: "hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
props.onSelectCompletion(idx);
|
||||||
|
props.onAcceptCompletion();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class={`text-xs px-1 rounded ${
|
||||||
|
comp.type === "command"
|
||||||
|
? "bg-purple-100 text-purple-700"
|
||||||
|
: comp.type === "option"
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-gray-100 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{comp.type}
|
||||||
|
</span>
|
||||||
|
<span class="font-mono text-sm">{comp.label}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={comp.description}>
|
||||||
|
<span class="text-xs text-gray-500">{comp.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { useCommander, defaultCommands, parseInput, getCompletions, getResultClass } from './useCommander';
|
||||||
|
export type { UseCommanderReturn } from './useCommander';
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
import { createSignal, createMemo } from "solid-js";
|
||||||
|
import {
|
||||||
|
MdCommanderCommand,
|
||||||
|
CommanderEntry,
|
||||||
|
CompletionItem,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
// ==================== 默认命令 ====================
|
||||||
|
|
||||||
|
export const defaultCommands: Record<string, MdCommanderCommand> = {
|
||||||
|
help: {
|
||||||
|
command: "help",
|
||||||
|
description: "显示帮助信息或特定命令的帮助",
|
||||||
|
options: {
|
||||||
|
cmd: {
|
||||||
|
option: "cmd",
|
||||||
|
description: "要查询的命令名",
|
||||||
|
type: "enum",
|
||||||
|
values: [], // 运行时填充
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (args) => {
|
||||||
|
if (args.cmd) {
|
||||||
|
return {
|
||||||
|
message: `命令:${args.cmd}\n描述:${defaultCommands[args.cmd]?.description || "无描述"}`,
|
||||||
|
type: "info",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const cmdList = Object.keys(defaultCommands).join(", ");
|
||||||
|
return {
|
||||||
|
message: `可用命令:${cmdList}`,
|
||||||
|
type: "info",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
command: "clear",
|
||||||
|
description: "清空命令历史",
|
||||||
|
handler: () => ({
|
||||||
|
message: "命令历史已清空",
|
||||||
|
type: "success",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
roll: {
|
||||||
|
command: "roll",
|
||||||
|
description: "掷骰子",
|
||||||
|
options: {
|
||||||
|
formula: {
|
||||||
|
option: "formula",
|
||||||
|
description: "骰子公式,如 2d6+3",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (args) => {
|
||||||
|
const formula = args.formula || "1d6";
|
||||||
|
const match = formula.match(/^(\d+)?d(\d+)([+-]\d+)?$/i);
|
||||||
|
if (!match) {
|
||||||
|
return { message: `无效的骰子公式:${formula}`, type: "error" };
|
||||||
|
}
|
||||||
|
const count = parseInt(match[1] || "1");
|
||||||
|
const sides = parseInt(match[2]);
|
||||||
|
const modifier = parseInt(match[3] || "0");
|
||||||
|
const rolls: number[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
rolls.push(Math.floor(Math.random() * sides) + 1);
|
||||||
|
}
|
||||||
|
const total = rolls.reduce((a, b) => a + b, 0) + modifier;
|
||||||
|
return {
|
||||||
|
message: `${formula} = [${rolls.join(", ")}]${modifier !== 0 ? (modifier > 0 ? `+${modifier}` : modifier) : ""} = ${total}`,
|
||||||
|
type: "success",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
export function parseInput(input: string): {
|
||||||
|
command?: string;
|
||||||
|
options: Record<string, string>;
|
||||||
|
incompleteOption?: string;
|
||||||
|
} {
|
||||||
|
const result: {
|
||||||
|
command?: string;
|
||||||
|
options: Record<string, string>;
|
||||||
|
incompleteOption?: string;
|
||||||
|
} = {
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) return result;
|
||||||
|
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
if (parts[0] && !parts[0].startsWith("-")) {
|
||||||
|
result.command = parts[0];
|
||||||
|
i = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < parts.length) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (part.startsWith("--")) {
|
||||||
|
const eqIndex = part.indexOf("=");
|
||||||
|
if (eqIndex !== -1) {
|
||||||
|
const key = part.slice(2, eqIndex);
|
||||||
|
const value = part.slice(eqIndex + 1);
|
||||||
|
result.options[key] = value;
|
||||||
|
} else {
|
||||||
|
const key = part.slice(2);
|
||||||
|
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
||||||
|
result.options[key] = parts[i + 1];
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
result.incompleteOption = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompletions(
|
||||||
|
input: string,
|
||||||
|
commands: Record<string, MdCommanderCommand>,
|
||||||
|
): CompletionItem[] {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
|
||||||
|
if (!trimmed || /^\s*$/.test(trimmed)) {
|
||||||
|
return Object.values(commands).map((cmd) => ({
|
||||||
|
label: cmd.command,
|
||||||
|
type: "command",
|
||||||
|
description: cmd.description,
|
||||||
|
insertText: cmd.command,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseInput(trimmed);
|
||||||
|
|
||||||
|
if (!parsed.command || !commands[parsed.command]) {
|
||||||
|
const commandCompletions = Object.values(commands)
|
||||||
|
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
|
||||||
|
.map((cmd) => ({
|
||||||
|
label: cmd.command,
|
||||||
|
type: "command",
|
||||||
|
description: cmd.description,
|
||||||
|
insertText: cmd.command,
|
||||||
|
}));
|
||||||
|
if (commandCompletions.length > 0) {
|
||||||
|
return commandCompletions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = commands[parsed.command!];
|
||||||
|
if (!cmd || !cmd.options) return [];
|
||||||
|
|
||||||
|
if (parsed.incompleteOption) {
|
||||||
|
const option = cmd.options[parsed.incompleteOption];
|
||||||
|
if (option?.type === "enum" && option.values) {
|
||||||
|
return option.values
|
||||||
|
.filter((v) => v.startsWith(parsed.incompleteOption))
|
||||||
|
.map((v) => ({
|
||||||
|
label: v,
|
||||||
|
type: "value",
|
||||||
|
insertText: v,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedOptions = Object.keys(parsed.options);
|
||||||
|
return Object.values(cmd.options)
|
||||||
|
.filter((opt) => !usedOptions.includes(opt.option))
|
||||||
|
.map((opt) => ({
|
||||||
|
label: `--${opt.option}`,
|
||||||
|
type: "option",
|
||||||
|
description: opt.description,
|
||||||
|
insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResultClass(
|
||||||
|
type?: "success" | "error" | "warning" | "info",
|
||||||
|
): string {
|
||||||
|
switch (type) {
|
||||||
|
case "success":
|
||||||
|
return "text-green-600";
|
||||||
|
case "error":
|
||||||
|
return "text-red-600";
|
||||||
|
case "warning":
|
||||||
|
return "text-yellow-600";
|
||||||
|
case "info":
|
||||||
|
default:
|
||||||
|
return "text-blue-600";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Commander Hook ====================
|
||||||
|
|
||||||
|
export interface UseCommanderReturn {
|
||||||
|
inputValue: () => string;
|
||||||
|
entries: () => CommanderEntry[];
|
||||||
|
showCompletions: () => boolean;
|
||||||
|
completions: () => CompletionItem[];
|
||||||
|
selectedCompletion: () => number;
|
||||||
|
isFocused: () => boolean;
|
||||||
|
setInputValue: (v: string) => void;
|
||||||
|
setEntries: (updater: (prev: CommanderEntry[]) => CommanderEntry[]) => void;
|
||||||
|
setShowCompletions: (v: boolean) => void;
|
||||||
|
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
|
||||||
|
setIsFocused: (v: boolean) => void;
|
||||||
|
handleCommand: () => void;
|
||||||
|
updateCompletions: () => void;
|
||||||
|
acceptCompletion: () => void;
|
||||||
|
commands: Record<string, MdCommanderCommand>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommander(
|
||||||
|
customCommands?: Record<string, MdCommanderCommand>,
|
||||||
|
): UseCommanderReturn {
|
||||||
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
|
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
||||||
|
const [showCompletions, setShowCompletions] = createSignal(false);
|
||||||
|
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
||||||
|
const [selectedCompletion, setSelectedCompletion] = createSignal(0);
|
||||||
|
const [isFocused, setIsFocused] = createSignal(false);
|
||||||
|
|
||||||
|
const commands = { ...defaultCommands, ...customCommands };
|
||||||
|
|
||||||
|
// 更新 help 命令的选项值
|
||||||
|
if (commands.help?.options?.cmd) {
|
||||||
|
commands.help.options.cmd.values = Object.keys(commands).filter(
|
||||||
|
(k) => k !== "help",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommand = () => {
|
||||||
|
const input = inputValue().trim();
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const parsed = parseInput(input);
|
||||||
|
const commandName = parsed.command;
|
||||||
|
const cmd = commands[commandName!];
|
||||||
|
|
||||||
|
let result: { message: string; type?: "success" | "error" | "warning" | "info" };
|
||||||
|
|
||||||
|
if (!cmd) {
|
||||||
|
result = { message: `未知命令:${commandName}`, type: "error" };
|
||||||
|
} else if (cmd.handler) {
|
||||||
|
try {
|
||||||
|
result = cmd.handler(parsed.options);
|
||||||
|
} catch (e) {
|
||||||
|
result = {
|
||||||
|
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
type: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = { message: `命令 ${commandName} 已执行(无处理器)`, type: "info" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEntry: CommanderEntry = {
|
||||||
|
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
||||||
|
command: input,
|
||||||
|
args: parsed.options,
|
||||||
|
result,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setEntries((prev) => [...prev, newEntry]);
|
||||||
|
setInputValue("");
|
||||||
|
setShowCompletions(false);
|
||||||
|
|
||||||
|
if (commandName === "clear") {
|
||||||
|
setEntries([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCompletions = () => {
|
||||||
|
const input = inputValue();
|
||||||
|
const comps = getCompletions(input, commands);
|
||||||
|
setCompletions(comps);
|
||||||
|
setShowCompletions(comps.length > 0 && isFocused());
|
||||||
|
setSelectedCompletion(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptCompletion = () => {
|
||||||
|
const idx = selectedCompletion();
|
||||||
|
const comp = completions()[idx];
|
||||||
|
if (!comp) return;
|
||||||
|
|
||||||
|
const input = inputValue();
|
||||||
|
const parsed = parseInput(input);
|
||||||
|
|
||||||
|
let newValue: string;
|
||||||
|
if (comp.type === "command") {
|
||||||
|
newValue = comp.insertText + " ";
|
||||||
|
} else if (comp.type === "option") {
|
||||||
|
const base = parsed.command || "";
|
||||||
|
const existingOptions = Object.entries(parsed.options)
|
||||||
|
.map(([k, v]) => `--${k}=${v}`)
|
||||||
|
.join(" ");
|
||||||
|
newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
|
||||||
|
} else {
|
||||||
|
const optKey = parsed.incompleteOption;
|
||||||
|
if (optKey) {
|
||||||
|
const base = parsed.command || "";
|
||||||
|
const otherOptions = Object.entries(parsed.options)
|
||||||
|
.filter(([k]) => k !== optKey)
|
||||||
|
.map(([k, v]) => `--${k}=${v}`)
|
||||||
|
.join(" ");
|
||||||
|
newValue = `${base} --${optKey}=${comp.insertText}${otherOptions ? " " + otherOptions : ""}`;
|
||||||
|
} else {
|
||||||
|
newValue = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue(newValue.trim());
|
||||||
|
setShowCompletions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputValue,
|
||||||
|
entries,
|
||||||
|
showCompletions,
|
||||||
|
completions,
|
||||||
|
selectedCompletion,
|
||||||
|
isFocused,
|
||||||
|
setInputValue,
|
||||||
|
setEntries,
|
||||||
|
setShowCompletions,
|
||||||
|
setSelectedCompletion,
|
||||||
|
setIsFocused,
|
||||||
|
handleCommand,
|
||||||
|
updateCompletions,
|
||||||
|
acceptCompletion,
|
||||||
|
commands,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
|
import { onMount, onCleanup } from "solid-js";
|
||||||
|
import { useCommander } from "./hooks";
|
||||||
|
import { CommanderInput } from "./CommanderInput";
|
||||||
|
import { CommanderEntries } from "./CommanderEntries";
|
||||||
|
import type { MdCommanderProps } from "./types";
|
||||||
|
|
||||||
|
export { CommanderInput } from './CommanderInput';
|
||||||
|
export type { CommanderInputProps } from './CommanderInput';
|
||||||
|
export { CommanderEntries } from './CommanderEntries';
|
||||||
|
export type { CommanderEntriesProps } from './CommanderEntries';
|
||||||
|
export type {
|
||||||
|
MdCommanderProps,
|
||||||
|
MdCommanderCommand,
|
||||||
|
MdCommanderOption,
|
||||||
|
MdCommanderOptionType,
|
||||||
|
CommanderEntry,
|
||||||
|
CompletionItem,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
customElement<MdCommanderProps>(
|
||||||
|
"md-commander",
|
||||||
|
{ placeholder: "", class: "", height: "" },
|
||||||
|
(props, { element }) => {
|
||||||
|
noShadowDOM();
|
||||||
|
|
||||||
|
const commander = useCommander(props.commands);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (commander.showCompletions() && commander.completions().length > 0) {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
commander.setSelectedCompletion(
|
||||||
|
(prev) => (prev + 1) % commander.completions().length,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
commander.setSelectedCompletion(
|
||||||
|
(prev) => (prev - 1 + commander.completions().length) % commander.completions().length,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
commander.acceptCompletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
commander.setShowCompletions(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
commander.handleCommand();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const heightStyle = () => props.height || "400px";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (!element?.contains(e.target as Node)) {
|
||||||
|
commander.setShowCompletions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
onCleanup(() => document.removeEventListener("click", handleClickOutside));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${props.class || ""}`}
|
||||||
|
style={{ height: heightStyle() }}
|
||||||
|
>
|
||||||
|
{/* 命令输入框 */}
|
||||||
|
<div class="relative">
|
||||||
|
<CommanderInput
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
inputValue={commander.inputValue}
|
||||||
|
onInput={(e) => {
|
||||||
|
commander.setInputValue((e.target as HTMLInputElement).value);
|
||||||
|
commander.updateCompletions();
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => {
|
||||||
|
commander.setIsFocused(true);
|
||||||
|
commander.updateCompletions();
|
||||||
|
}}
|
||||||
|
onBlur={() => commander.setIsFocused(false)}
|
||||||
|
onSubmit={commander.handleCommand}
|
||||||
|
showCompletions={commander.showCompletions}
|
||||||
|
completions={commander.completions}
|
||||||
|
selectedCompletion={commander.selectedCompletion}
|
||||||
|
onSelectCompletion={commander.setSelectedCompletion}
|
||||||
|
onAcceptCompletion={commander.acceptCompletion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 命令执行结果 */}
|
||||||
|
<CommanderEntries entries={commander.entries} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { Component } from "solid-js";
|
||||||
|
|
||||||
|
export interface MdCommanderProps {
|
||||||
|
placeholder?: string;
|
||||||
|
class?: string;
|
||||||
|
height?: string;
|
||||||
|
commands?: Record<string, MdCommanderCommand>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MdCommanderCommand {
|
||||||
|
command: string;
|
||||||
|
description?: string;
|
||||||
|
options?: Record<string, MdCommanderOption>;
|
||||||
|
handler?: (args: Record<string, any>) => {
|
||||||
|
message: string;
|
||||||
|
type?: "success" | "error" | "info" | "warning";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MdCommanderOptionType =
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "enum"
|
||||||
|
| "tag"
|
||||||
|
| "boolean";
|
||||||
|
|
||||||
|
export interface MdCommanderOption {
|
||||||
|
option: string;
|
||||||
|
description?: string;
|
||||||
|
type: MdCommanderOptionType;
|
||||||
|
required?: boolean;
|
||||||
|
default?: any;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
values?: string[];
|
||||||
|
EntryComponent?: Component<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommanderEntry {
|
||||||
|
id: string;
|
||||||
|
command: string;
|
||||||
|
args: Record<string, any>;
|
||||||
|
result: {
|
||||||
|
message: string;
|
||||||
|
type?: "success" | "error" | "warning" | "info";
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompletionItem {
|
||||||
|
label: string;
|
||||||
|
type: "command" | "option" | "value";
|
||||||
|
description?: string;
|
||||||
|
insertText: string;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue