Compare commits
12 Commits
abaee79198
...
fa1c6d19e9
| Author | SHA1 | Date |
|---|---|---|
|
|
fa1c6d19e9 | |
|
|
bda201ae81 | |
|
|
62780e3e56 | |
|
|
5d026dfd80 | |
|
|
c2c3956a82 | |
|
|
3e14bff1e9 | |
|
|
aac7fe575a | |
|
|
660f820f4c | |
|
|
c977f366ea | |
|
|
05c93e0301 | |
|
|
90b0346b65 | |
|
|
1cd85adff9 |
12
QWEN.md
12
QWEN.md
|
|
@ -48,3 +48,15 @@ cli应当搜索目录下的所有`.md`文件,并为每个文件创建一条路
|
||||||
使用`@tailwindcss/typography`来管理`markdown`样式。
|
使用`@tailwindcss/typography`来管理`markdown`样式。
|
||||||
|
|
||||||
编写自定义UI组件时,不要使用`shadow dom`,并且尽可能继承`tailwindcss`指定的样式。
|
编写自定义UI组件时,不要使用`shadow dom`,并且尽可能继承`tailwindcss`指定的样式。
|
||||||
|
|
||||||
|
## LLM开发须知
|
||||||
|
|
||||||
|
开发环境主要在windows上,编辑时需要注意windows的换行符,以及命令行工具。
|
||||||
|
|
||||||
|
完成开发后,不仅要使用`npm run build`检查验证,还应该检查是否有类型错误。
|
||||||
|
|
||||||
|
若遇到bug且可以通过独立测试样例来检验,则在开发中自行创建临时测试样例测试。
|
||||||
|
|
||||||
|
solid.js倾向于使用`createEffect`和/或`createMemo`,以及在jsx中inline引用`props`,来实现响应式处理。
|
||||||
|
|
||||||
|
如果可以,优先使用`createEffect`和`createMemo`,而不是`onMount`+`createSignal`。
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# Commander Test
|
# Commander Test
|
||||||
|
|
||||||
:md-commander[]
|
:md-commander[./commands.csv]
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
command,parameter,label,description,insertedText
|
||||||
|
track,emmet,monk,破戒佛爷,"破戒佛爷.bandit.monk[gd=4/4,str=14/14,cla=10,spi=8]"
|
||||||
|
track,emmet,bandit,龙虎寨山贼,"龙虎寨山贼.bandit[gd=2/2,str=10/10,cla=10,spi=10]"
|
||||||
|
track,emmet,khitan,契丹护卫,"契丹护卫.khitan.guard[gd=4/4,ap=2,str=14/14,cla=14,spi=14]"
|
||||||
|
|
|
@ -39,4 +39,5 @@ export { TabBar } from './md-commander/TabBar';
|
||||||
export { TrackerView } from './md-commander/TrackerView';
|
export { TrackerView } from './md-commander/TrackerView';
|
||||||
export { CommanderEntries } from './md-commander/CommanderEntries';
|
export { CommanderEntries } from './md-commander/CommanderEntries';
|
||||||
export { CommanderInput } from './md-commander/CommanderInput';
|
export { CommanderInput } from './md-commander/CommanderInput';
|
||||||
export { useCommander, defaultCommands } from './md-commander/hooks';
|
export { useCommander } from './md-commander/hooks';
|
||||||
|
export { initializeCommands, getCommands } from './md-commander/stores/commandsStore';
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
import { type Component, For, Show, createEffect, on } from "solid-js";
|
import { type Component, For, Show } from "solid-js";
|
||||||
import type { CommanderEntry } from "./types";
|
import type { CommanderEntry } from "./types";
|
||||||
import { getResultClass } from "./hooks";
|
import { getResultClass } from "./hooks";
|
||||||
|
|
||||||
export interface CommanderEntriesProps {
|
export interface CommanderEntriesProps {
|
||||||
entries: () => CommanderEntry[];
|
entries: () => CommanderEntry[];
|
||||||
onCommandClick?: (command: string) => void;
|
onCommandClick?: (command: string) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
||||||
let containerRef: HTMLDivElement | undefined;
|
let containerRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
// 当 entries 变化时自动滚动到底部
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.entries().length,
|
|
||||||
() => {
|
|
||||||
if (containerRef) {
|
|
||||||
containerRef.scrollTop = containerRef.scrollHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCommandClick = (command: string) => {
|
const handleCommandClick = (command: string) => {
|
||||||
if (props.onCommandClick) {
|
if (props.onCommandClick) {
|
||||||
props.onCommandClick(command);
|
props.onCommandClick(command);
|
||||||
|
|
@ -33,6 +23,12 @@ export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2"
|
class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2"
|
||||||
>
|
>
|
||||||
|
<Show
|
||||||
|
when={props.loading}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
when={props.error}
|
||||||
|
fallback={
|
||||||
<Show
|
<Show
|
||||||
when={props.entries().length > 0}
|
when={props.entries().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
|
|
@ -64,6 +60,19 @@ export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="text-red-500 text-center py-8">{props.error}</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<div class="text-gray-500 flex items-center gap-2">
|
||||||
|
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||||||
|
<span>加载命令模板中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {type Component, For, Show} from "solid-js";
|
import {type Component, For, Show, createEffect, on} from "solid-js";
|
||||||
import type { CompletionItem } from "./types";
|
import type { CompletionItem } from "./types";
|
||||||
|
|
||||||
export interface CommanderInputProps {
|
export interface CommanderInputProps {
|
||||||
|
|
@ -17,6 +17,25 @@ export interface CommanderInputProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
||||||
|
let containerRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
// 当选中项变化时自动滚动到可见区域
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => props.selectedCompletion(),
|
||||||
|
() => {
|
||||||
|
if (containerRef) {
|
||||||
|
const selectedItem = containerRef.querySelector('[data-selected="true"]');
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxVisible = 3;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center border-b border-gray-300 bg-gray-50 px-3 py-2">
|
<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>
|
<span class="text-gray-500 mr-2">❯</span>
|
||||||
|
|
@ -39,9 +58,14 @@ export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
||||||
|
|
||||||
{/* 自动补全下拉框 - 向上弹出 */}
|
{/* 自动补全下拉框 - 向上弹出 */}
|
||||||
<Show when={props.showCompletions() && props.completions().length > 0}>
|
<Show when={props.showCompletions() && props.completions().length > 0}>
|
||||||
<div class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg mb-1">
|
|
||||||
<For each={props.completions().slice(0, 3)}>{(comp, idx) => (
|
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg mb-1 overflow-auto"
|
||||||
|
style={{ "max-height": `${maxVisible * 40}px` }}
|
||||||
|
>
|
||||||
|
<For each={props.completions()}>{(comp, idx) => (
|
||||||
|
<div
|
||||||
|
data-selected={idx() === props.selectedCompletion()}
|
||||||
class={`px-3 py-2 cursor-pointer flex justify-between items-center ${
|
class={`px-3 py-2 cursor-pointer flex justify-between items-center ${
|
||||||
idx() === props.selectedCompletion()
|
idx() === props.selectedCompletion()
|
||||||
? "bg-blue-100"
|
? "bg-blue-100"
|
||||||
|
|
@ -71,11 +95,6 @@ export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}</For>
|
)}</For>
|
||||||
<Show when={props.completions().length > 3}>
|
|
||||||
<div class="px-3 py-1 text-xs text-gray-500 bg-gray-50 border-t border-gray-200">
|
|
||||||
还有 {props.completions().length - 3} 个补全项...
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export interface TabBarProps {
|
||||||
|
|
||||||
export const TabBar: Component<TabBarProps> = (props) => {
|
export const TabBar: Component<TabBarProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex border-b border-gray-300 bg-gray-50">
|
<div class="flex lg:hidden border-b border-gray-300 bg-gray-50">
|
||||||
<button
|
<button
|
||||||
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
props.mode() === "history"
|
props.mode() === "history"
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
|
||||||
return `${name}=${val}`;
|
return `${name}=${val}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAttributeClick = (e: MouseEvent, index: number, attrName: string, attr: TrackerAttribute) => {
|
const handleTagClick = (e: MouseEvent, index: number) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
setEditingItem({
|
setEditingItem({
|
||||||
index,
|
index,
|
||||||
position: {
|
position: {
|
||||||
|
|
@ -76,32 +76,23 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
|
||||||
{/* 第一行:tag#id.class + 操作按钮 */}
|
{/* 第一行:tag#id.class + 操作按钮 */}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span class="font-bold text-gray-800 whitespace-nowrap">{item.tag}</span>
|
<button
|
||||||
|
class="font-bold text-gray-800 whitespace-nowrap hover:text-blue-600 hover:underline cursor-pointer transition-colors"
|
||||||
|
title="点击编辑属性"
|
||||||
|
onClick={(e) => handleTagClick(e, index())}
|
||||||
|
>
|
||||||
|
{item.tag}
|
||||||
|
</button>
|
||||||
<Show when={item.id}>
|
<Show when={item.id}>
|
||||||
<span class="text-xs text-purple-600 font-mono whitespace-nowrap">#{item.id}</span>
|
<span class="text-xs text-purple-600 font-mono whitespace-nowrap">#{item.id}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={item.classes.length > 0}>
|
<Show when={item.classes.length > 0}>
|
||||||
<span class="text-xs text-blue-600 font-mono whitespace-nowrap">.{item.classes.join(".")}</span>
|
<span class="text-xs text-blue-600 font-mono whitespace-nowrap">.{item.classes.join(".")}</span>
|
||||||
</Show>
|
</Show>
|
||||||
{/* 属性链接 - 可点击编辑 */}
|
{/* 属性简写显示 */}
|
||||||
<Show when={Object.keys(item.attributes).length > 0}>
|
<Show when={Object.keys(item.attributes).length > 0}>
|
||||||
<span class="text-xs font-mono whitespace-nowrap truncate">
|
<span class="text-xs text-gray-500 font-mono whitespace-nowrap truncate">
|
||||||
[
|
[{Object.entries(item.attributes).map(([k, v]) => formatAttributeShort(k, v)).join(" ")}]
|
||||||
<For each={Object.entries(item.attributes)}>
|
|
||||||
{([name, attr], attrIndex) => (
|
|
||||||
<>
|
|
||||||
<Show when={attrIndex() > 0}> </Show>
|
|
||||||
<button
|
|
||||||
class="text-gray-500 hover:text-blue-600 hover:underline cursor-pointer transition-colors"
|
|
||||||
title={`点击编辑 ${name}`}
|
|
||||||
onClick={(e) => handleAttributeClick(e, index(), name, attr)}
|
|
||||||
>
|
|
||||||
{name}={formatAttributeValue(attr)}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
]
|
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
||||||
|
|
||||||
export const helpCommand: MdCommanderCommand = {
|
export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand {
|
||||||
|
return {
|
||||||
command: "help",
|
command: "help",
|
||||||
description: "显示帮助信息或特定命令的帮助",
|
description: "显示帮助信息或特定命令的帮助",
|
||||||
parameters: [
|
parameters: [
|
||||||
|
|
@ -8,30 +9,23 @@ export const helpCommand: MdCommanderCommand = {
|
||||||
name: "cmd",
|
name: "cmd",
|
||||||
description: "要查询的命令名",
|
description: "要查询的命令名",
|
||||||
type: "enum",
|
type: "enum",
|
||||||
values: [], // 运行时填充
|
values: Object.keys(commands).filter((k) => k !== "help"),
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
handler: (args, commands) => {
|
handler: (args, cmds) => {
|
||||||
const cmdName = args.params.cmd;
|
const cmdName = args.params.cmd;
|
||||||
if (cmdName && commands?.[cmdName]) {
|
if (cmdName && cmds?.[cmdName]) {
|
||||||
return {
|
return {
|
||||||
message: `命令:${cmdName}\n描述:${commands[cmdName]?.description || "无描述"}`,
|
message: `命令:${cmdName}\n描述:${cmds[cmdName]?.description || "无描述"}`,
|
||||||
type: "info",
|
type: "info",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const cmdList = Object.keys(commands || {}).filter(k => k !== "help").join(", ");
|
const cmdList = Object.keys(cmds || {}).filter((k) => k !== "help").join(", ");
|
||||||
return {
|
return {
|
||||||
message: `可用命令:${cmdList}`,
|
message: `可用命令:${cmdList}`,
|
||||||
type: "info",
|
type: "info",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand {
|
|
||||||
const cmd = { ...helpCommand };
|
|
||||||
if (cmd.parameters?.[0]) {
|
|
||||||
cmd.parameters[0].values = Object.keys(commands).filter(k => k !== "help");
|
|
||||||
}
|
|
||||||
return cmd;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { helpCommand, setupHelpCommand } from "./help";
|
export { setupHelpCommand } from "./help";
|
||||||
export { clearCommand } from "./clear";
|
export { clearCommand } from "./clear";
|
||||||
export { rollCommand } from "./roll";
|
export { rollCommand } from "./roll";
|
||||||
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";
|
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
import type { MdCommanderCommandMap, CompletionItem } from "../types";
|
||||||
|
|
||||||
|
export interface ParsedCommand {
|
||||||
|
command?: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
options: Record<string, string>;
|
||||||
|
incompleteParam?: { index: number; value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命令行输入
|
||||||
|
*/
|
||||||
|
export function parseInput(input: string, commands?: MdCommanderCommandMap): ParsedCommand {
|
||||||
|
const result: ParsedCommand = { params: {}, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取命令的参数定义
|
||||||
|
const cmd = result.command ? commands?.[result.command] : undefined;
|
||||||
|
const paramDefs = cmd?.parameters || [];
|
||||||
|
let paramIndex = 0;
|
||||||
|
|
||||||
|
// 解析参数和选项
|
||||||
|
while (i < parts.length) {
|
||||||
|
const part = parts[i];
|
||||||
|
|
||||||
|
if (part.startsWith("--")) {
|
||||||
|
const eqIndex = part.indexOf("=");
|
||||||
|
if (eqIndex !== -1) {
|
||||||
|
result.options[part.slice(2, eqIndex)] = part.slice(eqIndex + 1);
|
||||||
|
} else {
|
||||||
|
const key = part.slice(2);
|
||||||
|
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
||||||
|
result.options[key] = parts[i + 1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (part.startsWith("-") && part.length === 2) {
|
||||||
|
const key = part.slice(1);
|
||||||
|
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
||||||
|
result.options[key] = parts[i + 1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else if (paramIndex < paramDefs.length) {
|
||||||
|
result.params[paramDefs[paramIndex].name] = part;
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有未完成的位置参数
|
||||||
|
if (paramIndex < paramDefs.length) {
|
||||||
|
const lastPart = parts[parts.length - 1];
|
||||||
|
if (lastPart && !lastPart.startsWith("-") && parts.length > 1) {
|
||||||
|
result.incompleteParam = { index: paramIndex, value: lastPart };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自动补全建议
|
||||||
|
*/
|
||||||
|
export function getCompletions(input: string, commands: MdCommanderCommandMap): 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, commands);
|
||||||
|
|
||||||
|
// 命令补全
|
||||||
|
if (!parsed.command || !commands[parsed.command]) {
|
||||||
|
return Object.values(commands)
|
||||||
|
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
|
||||||
|
.map((cmd) => ({
|
||||||
|
label: cmd.command,
|
||||||
|
type: "command",
|
||||||
|
description: cmd.description,
|
||||||
|
insertText: cmd.command,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = commands[parsed.command];
|
||||||
|
if (!cmd) return [];
|
||||||
|
|
||||||
|
const paramDefs = cmd.parameters || [];
|
||||||
|
const usedParams = Object.keys(parsed.params);
|
||||||
|
|
||||||
|
// 判断是否正在输入最后一个参数
|
||||||
|
const isTypingLastParam = paramDefs.length === usedParams.length &&
|
||||||
|
trimmed.length > 0 &&
|
||||||
|
!trimmed.endsWith(" ") &&
|
||||||
|
!trimmed.split(/\s+/)[trimmed.split(/\s+/).length - 1].startsWith("-");
|
||||||
|
|
||||||
|
// 参数补全
|
||||||
|
if (paramDefs.length > usedParams.length || parsed.incompleteParam || isTypingLastParam) {
|
||||||
|
let paramIndex = parsed.incompleteParam?.index ?? (isTypingLastParam ? paramDefs.length - 1 : usedParams.length);
|
||||||
|
const paramDef = paramDefs[paramIndex];
|
||||||
|
if (!paramDef) return [];
|
||||||
|
|
||||||
|
let currentValue = parsed.incompleteParam?.value || "";
|
||||||
|
if (isTypingLastParam && !parsed.incompleteParam) {
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
currentValue = parts[parts.length - 1] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板补全
|
||||||
|
if (paramDef.templates) {
|
||||||
|
return paramDef.templates
|
||||||
|
.filter((t) => t.label.toLowerCase().includes(currentValue.toLowerCase()))
|
||||||
|
.map((t) => ({
|
||||||
|
label: t.label,
|
||||||
|
type: "value",
|
||||||
|
description: t.description,
|
||||||
|
insertText: t.insertText,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举补全
|
||||||
|
if (paramDef.type === "enum" && paramDef.values) {
|
||||||
|
return paramDef.values
|
||||||
|
.filter((v) => v.startsWith(currentValue))
|
||||||
|
.map((v) => ({
|
||||||
|
label: v,
|
||||||
|
type: "value",
|
||||||
|
description: paramDef.description,
|
||||||
|
insertText: v,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他类型显示提示
|
||||||
|
return [{
|
||||||
|
label: `<${paramDef.name}>`,
|
||||||
|
type: "value",
|
||||||
|
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
|
||||||
|
insertText: "",
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选项补全
|
||||||
|
if (!cmd.options) return [];
|
||||||
|
|
||||||
|
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" ? "" : ""}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据结果类型获取 CSS 类名
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
default: return "text-blue-600";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
export { useCommander, defaultCommands, parseInput, getCompletions, getResultClass } from './useCommander';
|
export { useCommander } from "./useCommander";
|
||||||
export type { UseCommanderReturn } from './useCommander';
|
export type { UseCommanderReturn } from "./useCommander";
|
||||||
export { rollFormula, rollSimple } from './useDiceRoller';
|
export { rollSimple, rollFormula } from "./useDiceRoller";
|
||||||
export * from './dice-engine';
|
export type { DiceRollerResult } from "./useDiceRoller";
|
||||||
|
export { parseInput, getCompletions, getResultClass } from "./completions";
|
||||||
// Tracker 相关导出
|
export type { ParsedCommand } from "./completions";
|
||||||
export type {
|
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerAttributeType,
|
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
|
||||||
} from '../types';
|
|
||||||
|
|
|
||||||
|
|
@ -1,226 +1,25 @@
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import {
|
import type {
|
||||||
MdCommanderCommand,
|
|
||||||
MdCommanderCommandMap,
|
|
||||||
CommanderEntry,
|
CommanderEntry,
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
TrackerItem,
|
TrackerItem,
|
||||||
TrackerAttribute,
|
TrackerAttribute,
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
TrackerViewMode,
|
||||||
|
MdCommanderCommandMap,
|
||||||
|
TrackerCommand,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { rollSimple } from "./useDiceRoller";
|
import { parseInput, getCompletions } from "./completions";
|
||||||
import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
|
|
||||||
import {
|
import {
|
||||||
addTrackerItem as addTracker,
|
addTrackerItem,
|
||||||
removeTrackerItem as removeTracker,
|
|
||||||
updateTrackerAttribute as updateTrackerAttr,
|
|
||||||
updateTrackerClasses as updateTrackerClassesStore,
|
|
||||||
moveTrackerItem as moveTracker,
|
|
||||||
removeTrackerClass as removeClassFromTracker,
|
|
||||||
getTrackerItems,
|
|
||||||
getTrackerHistory,
|
getTrackerHistory,
|
||||||
findTrackerIndex,
|
getTrackerItems,
|
||||||
|
moveTrackerItem,
|
||||||
|
removeTrackerClass,
|
||||||
|
removeTrackerItem,
|
||||||
|
updateTrackerAttribute,
|
||||||
|
updateTrackerClasses,
|
||||||
} from "../stores";
|
} from "../stores";
|
||||||
|
import { addEntry, getEntries, clearEntries } from "../stores/entriesStore";
|
||||||
// ==================== 默认命令 ====================
|
|
||||||
|
|
||||||
export const defaultCommands: MdCommanderCommandMap = {
|
|
||||||
help: setupHelpCommand({}),
|
|
||||||
clear: clearCommand,
|
|
||||||
roll: rollCommand,
|
|
||||||
track: trackCommand,
|
|
||||||
untrack: untrackCommand,
|
|
||||||
list: listTrackCommand,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化默认命令
|
|
||||||
Object.keys(defaultCommands).forEach(key => {
|
|
||||||
if (key === "help") {
|
|
||||||
const help = setupHelpCommand(defaultCommands);
|
|
||||||
defaultCommands.help = help;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
|
||||||
|
|
||||||
export function parseInput(input: string, commands?: MdCommanderCommandMap): {
|
|
||||||
command?: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
options: Record<string, string>;
|
|
||||||
incompleteParam?: { index: number; value: string };
|
|
||||||
} {
|
|
||||||
const result: {
|
|
||||||
command?: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
options: Record<string, string>;
|
|
||||||
incompleteParam?: { index: number; value: string };
|
|
||||||
} = {
|
|
||||||
params: {},
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取命令的参数定义
|
|
||||||
const cmd = result.command ? commands?.[result.command] : undefined;
|
|
||||||
const paramDefs = cmd?.parameters || [];
|
|
||||||
let paramIndex = 0;
|
|
||||||
|
|
||||||
// 解析参数和选项
|
|
||||||
while (i < parts.length) {
|
|
||||||
const part = parts[i];
|
|
||||||
|
|
||||||
if (part.startsWith("--")) {
|
|
||||||
// 选项 --key=value 或 --key value
|
|
||||||
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 {
|
|
||||||
// 未完成的选项
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (part.startsWith("-") && part.length === 2) {
|
|
||||||
// 短选项 -k value
|
|
||||||
const key = part.slice(1);
|
|
||||||
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
|
||||||
result.options[key] = parts[i + 1];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 位置参数
|
|
||||||
if (paramIndex < paramDefs.length) {
|
|
||||||
const paramDef = paramDefs[paramIndex];
|
|
||||||
result.params[paramDef.name] = part;
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有未完成的位置参数
|
|
||||||
if (paramIndex < paramDefs.length) {
|
|
||||||
const lastPart = parts[parts.length - 1];
|
|
||||||
if (lastPart && !lastPart.startsWith("-")) {
|
|
||||||
result.incompleteParam = {
|
|
||||||
index: paramIndex,
|
|
||||||
value: lastPart,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCompletions(
|
|
||||||
input: string,
|
|
||||||
commands: MdCommanderCommandMap,
|
|
||||||
): 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, commands);
|
|
||||||
|
|
||||||
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" as "command",
|
|
||||||
description: cmd.description,
|
|
||||||
insertText: cmd.command,
|
|
||||||
}));
|
|
||||||
if (commandCompletions.length > 0) {
|
|
||||||
return commandCompletions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd = commands[parsed.command!];
|
|
||||||
if (!cmd) return [];
|
|
||||||
|
|
||||||
// 检查是否需要参数补全
|
|
||||||
const paramDefs = cmd.parameters || [];
|
|
||||||
const usedParams = Object.keys(parsed.params);
|
|
||||||
|
|
||||||
// 如果还有未填的参数,提供参数值补全
|
|
||||||
if (paramDefs.length > usedParams.length) {
|
|
||||||
const paramDef = paramDefs[usedParams.length];
|
|
||||||
if (paramDef.type === "enum" && paramDef.values) {
|
|
||||||
const currentValue = parsed.incompleteParam?.value || "";
|
|
||||||
return paramDef.values
|
|
||||||
.filter((v) => v.startsWith(currentValue))
|
|
||||||
.map((v) => ({
|
|
||||||
label: v,
|
|
||||||
type: "value",
|
|
||||||
description: paramDef.description,
|
|
||||||
insertText: v,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
// 其他类型的参数,显示提示
|
|
||||||
if (parsed.incompleteParam) {
|
|
||||||
return [{
|
|
||||||
label: `<${paramDef.name}>`,
|
|
||||||
type: "value",
|
|
||||||
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
|
|
||||||
insertText: "",
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选项补全
|
|
||||||
if (!cmd.options) return [];
|
|
||||||
|
|
||||||
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 ====================
|
// ==================== Commander Hook ====================
|
||||||
|
|
||||||
|
|
@ -232,71 +31,60 @@ export interface UseCommanderReturn {
|
||||||
selectedCompletion: () => number;
|
selectedCompletion: () => number;
|
||||||
isFocused: () => boolean;
|
isFocused: () => boolean;
|
||||||
setInputValue: (v: string) => void;
|
setInputValue: (v: string) => void;
|
||||||
setEntries: (updater: (prev: CommanderEntry[]) => CommanderEntry[]) => void;
|
|
||||||
setShowCompletions: (v: boolean) => void;
|
setShowCompletions: (v: boolean) => void;
|
||||||
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
|
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
|
||||||
setIsFocused: (v: boolean) => void;
|
setIsFocused: (v: boolean) => void;
|
||||||
handleCommand: () => void;
|
handleCommand: () => void;
|
||||||
updateCompletions: () => void;
|
updateCompletions: () => void;
|
||||||
acceptCompletion: () => void;
|
acceptCompletion: () => void;
|
||||||
commands: MdCommanderCommandMap;
|
commands: () => MdCommanderCommandMap;
|
||||||
|
setCommands: (cmds: MdCommanderCommandMap) => void;
|
||||||
historyIndex: () => number;
|
historyIndex: () => number;
|
||||||
setHistoryIndex: (v: number) => void;
|
setHistoryIndex: (v: number) => void;
|
||||||
commandHistory: () => string[];
|
commandHistory: () => string[];
|
||||||
navigateHistory: (direction: 'up' | 'down') => void;
|
navigateHistory: (direction: "up" | "down") => void;
|
||||||
|
|
||||||
// Tracker 相关
|
// Tracker 相关
|
||||||
viewMode: () => TrackerViewMode;
|
viewMode: () => TrackerViewMode;
|
||||||
setViewMode: (mode: TrackerViewMode) => void;
|
setViewMode: (mode: TrackerViewMode) => void;
|
||||||
trackerItems: () => TrackerItem[];
|
trackerItems: () => TrackerItem[];
|
||||||
setTrackerItems: (updater: (prev: TrackerItem[]) => TrackerItem[]) => void;
|
|
||||||
trackerHistory: () => TrackerCommand[];
|
trackerHistory: () => TrackerCommand[];
|
||||||
addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void;
|
addTrackerItem: (item: Omit<TrackerItem, "id">) => TrackerItem;
|
||||||
removeTrackerItem: (emmet: string) => boolean;
|
removeTrackerItem: (emmet: string) => boolean;
|
||||||
removeTrackerItemByIndex: (index: number) => void;
|
removeTrackerItemByIndex: (index: number) => void;
|
||||||
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
|
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
|
||||||
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
||||||
updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
|
updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
|
||||||
moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean;
|
moveTrackerItem: (emmet: string, direction: "up" | "down") => boolean;
|
||||||
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void;
|
moveTrackerItemByIndex: (index: number, direction: "up" | "down") => void;
|
||||||
removeTrackerItemClass: (emmet: string, className: string) => boolean;
|
removeTrackerItemClass: (emmet: string, className: string) => boolean;
|
||||||
removeTrackerItemClassByIndex: (index: number, className: string) => void;
|
removeTrackerItemClassByIndex: (index: number, className: string) => void;
|
||||||
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommander(
|
export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn {
|
||||||
customCommands?: MdCommanderCommandMap,
|
|
||||||
): UseCommanderReturn {
|
|
||||||
const [inputValue, setInputValue] = createSignal("");
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
|
||||||
const [showCompletions, setShowCompletions] = createSignal(false);
|
const [showCompletions, setShowCompletions] = createSignal(false);
|
||||||
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
||||||
const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
|
const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
|
||||||
const [isFocused, setIsFocused] = createSignal(false);
|
const [isFocused, setIsFocused] = createSignal(false);
|
||||||
|
|
||||||
// 命令历史
|
|
||||||
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
|
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
|
||||||
const [historyIndex, setHistoryIndex] = createSignal(-1);
|
const [historyIndex, setHistoryIndex] = createSignal(-1);
|
||||||
|
|
||||||
// Tracker 相关
|
|
||||||
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
|
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
|
||||||
|
const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {});
|
||||||
|
|
||||||
const commands = { ...defaultCommands, ...customCommands };
|
// 从 store 获取 entries
|
||||||
|
const entries = getEntries;
|
||||||
|
|
||||||
// 更新 help 命令的参数值
|
// ==================== 命令执行 ====================
|
||||||
if (commands.help?.parameters?.[0]) {
|
|
||||||
commands.help.parameters[0].values = Object.keys(commands).filter(
|
|
||||||
(k) => k !== "help",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCommand = () => {
|
const handleCommand = () => {
|
||||||
const input = inputValue().trim();
|
const input = inputValue().trim();
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const parsed = parseInput(input, commands);
|
const cmds = commands();
|
||||||
|
const parsed = parseInput(input, cmds);
|
||||||
const commandName = parsed.command;
|
const commandName = parsed.command;
|
||||||
const cmd = commands[commandName!];
|
const cmd = cmds[commandName!];
|
||||||
|
|
||||||
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
|
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
|
||||||
|
|
||||||
|
|
@ -304,7 +92,7 @@ export function useCommander(
|
||||||
result = { message: `未知命令:${commandName}`, type: "error" };
|
result = { message: `未知命令:${commandName}`, type: "error" };
|
||||||
} else if (cmd.handler) {
|
} else if (cmd.handler) {
|
||||||
try {
|
try {
|
||||||
result = cmd.handler({ params: parsed.params, options: parsed.options }, commands);
|
result = cmd.handler({ params: parsed.params, options: parsed.options }, cmds);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = {
|
result = {
|
||||||
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
|
@ -323,35 +111,31 @@ export function useCommander(
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setEntries((prev) => [...prev, newEntry]);
|
// 使用 store 添加记录
|
||||||
|
addEntry(newEntry);
|
||||||
// 添加到命令历史
|
|
||||||
setCommandHistory((prev) => [...prev, input]);
|
setCommandHistory((prev) => [...prev, input]);
|
||||||
setHistoryIndex(-1); // 重置历史索引
|
setHistoryIndex(-1);
|
||||||
|
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setShowCompletions(false);
|
setShowCompletions(false);
|
||||||
|
|
||||||
if (commandName === "clear") {
|
if (commandName === "clear") {
|
||||||
setEntries([]);
|
clearEntries();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== 自动补全 ====================
|
||||||
|
|
||||||
const updateCompletions = () => {
|
const updateCompletions = () => {
|
||||||
const input = inputValue();
|
const comps = getCompletions(inputValue(), commands());
|
||||||
const comps = getCompletions(input, commands);
|
|
||||||
setCompletions(comps);
|
setCompletions(comps);
|
||||||
setShowCompletions(comps.length > 0 && isFocused());
|
setShowCompletions(comps.length > 0 && isFocused());
|
||||||
setSelectedCompletionState(0);
|
setSelectedCompletionState(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
|
const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
|
||||||
const comps = completions();
|
const maxIdx = completions().length - 1;
|
||||||
const maxVisible = 3;
|
if (typeof v === "function") {
|
||||||
const maxIdx = Math.min(comps.length - 1, maxVisible - 1);
|
setSelectedCompletionState((prev) => {
|
||||||
|
|
||||||
if (typeof v === 'function') {
|
|
||||||
setSelectedCompletionState(prev => {
|
|
||||||
const next = v(prev);
|
const next = v(prev);
|
||||||
return Math.max(0, Math.min(next, maxIdx));
|
return Math.max(0, Math.min(next, maxIdx));
|
||||||
});
|
});
|
||||||
|
|
@ -363,37 +147,33 @@ export function useCommander(
|
||||||
const acceptCompletion = () => {
|
const acceptCompletion = () => {
|
||||||
const idx = selectedCompletion();
|
const idx = selectedCompletion();
|
||||||
const comps = completions();
|
const comps = completions();
|
||||||
// 确保索引在有效范围内
|
const comp = comps[Math.min(idx, comps.length - 1)];
|
||||||
const validIdx = Math.min(idx, Math.min(comps.length - 1, 2));
|
|
||||||
const comp = comps[validIdx];
|
|
||||||
if (!comp) return;
|
if (!comp) return;
|
||||||
|
|
||||||
const input = inputValue();
|
const input = inputValue();
|
||||||
const parsed = parseInput(input, commands);
|
const parsed = parseInput(input, commands());
|
||||||
|
|
||||||
let newValue: string;
|
let newValue: string;
|
||||||
|
|
||||||
if (comp.type === "command") {
|
if (comp.type === "command") {
|
||||||
newValue = comp.insertText + " ";
|
newValue = comp.insertText + " ";
|
||||||
} else if (comp.type === "option") {
|
} else if (comp.type === "option") {
|
||||||
const base = parsed.command || "";
|
|
||||||
const existingOptions = Object.entries(parsed.options)
|
const existingOptions = Object.entries(parsed.options)
|
||||||
.map(([k, v]) => `--${k}=${v}`)
|
.map(([k, v]) => `--${k}=${v}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
|
newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
|
||||||
} else if (comp.type === "value") {
|
} else if (comp.type === "value") {
|
||||||
// 参数值补全
|
const cmd = parsed.command ? commands()[parsed.command] : null;
|
||||||
const cmd = parsed.command ? commands[parsed.command] : null;
|
|
||||||
const paramDefs = cmd?.parameters || [];
|
const paramDefs = cmd?.parameters || [];
|
||||||
const usedParams = Object.keys(parsed.params);
|
const usedParams = Object.keys(parsed.params);
|
||||||
|
const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" ");
|
||||||
|
|
||||||
if (paramDefs.length > usedParams.length) {
|
if (isTypingLastParam) {
|
||||||
// 当前参数的补全
|
const parts = input.trim().split(/\s+/);
|
||||||
const base = parsed.command || "";
|
parts.pop();
|
||||||
|
newValue = `${parsed.command || ""}${parts.length > 1 ? " " + parts.slice(1).join(" ") : ""} ${comp.insertText}`;
|
||||||
|
} else if (paramDefs.length > usedParams.length) {
|
||||||
const existingParams = Object.values(parsed.params).join(" ");
|
const existingParams = Object.values(parsed.params).join(" ");
|
||||||
const existingOptions = Object.entries(parsed.options)
|
newValue = `${parsed.command || ""} ${existingParams}${existingParams ? " " : ""}${comp.insertText}`;
|
||||||
.map(([k, v]) => `--${k}=${v}`)
|
|
||||||
.join(" ");
|
|
||||||
newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`;
|
|
||||||
} else {
|
} else {
|
||||||
newValue = input;
|
newValue = input;
|
||||||
}
|
}
|
||||||
|
|
@ -405,22 +185,19 @@ export function useCommander(
|
||||||
setShowCompletions(false);
|
setShowCompletions(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateHistory = (direction: 'up' | 'down') => {
|
// ==================== 命令历史 ====================
|
||||||
|
|
||||||
|
const navigateHistory = (direction: "up" | "down") => {
|
||||||
const history = commandHistory();
|
const history = commandHistory();
|
||||||
if (history.length === 0) return;
|
if (history.length === 0) return;
|
||||||
|
|
||||||
let newIndex = historyIndex();
|
let newIndex = historyIndex();
|
||||||
if (direction === 'up') {
|
if (direction === "up") {
|
||||||
// 向上浏览历史(更早的命令)
|
if (newIndex < history.length - 1) newIndex++;
|
||||||
if (newIndex < history.length - 1) {
|
|
||||||
newIndex++;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 向下浏览历史(更新的命令)
|
|
||||||
if (newIndex > 0) {
|
if (newIndex > 0) {
|
||||||
newIndex--;
|
newIndex--;
|
||||||
} else {
|
} else {
|
||||||
// 回到当前输入
|
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
return;
|
return;
|
||||||
|
|
@ -428,92 +205,41 @@ export function useCommander(
|
||||||
}
|
}
|
||||||
|
|
||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
// 从历史末尾获取命令(最新的在前)
|
|
||||||
setInputValue(history[history.length - 1 - newIndex]);
|
setInputValue(history[history.length - 1 - newIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== Tracker 方法 ====================
|
// ==================== Tracker 操作 ====================
|
||||||
|
|
||||||
const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => {
|
const getEmmetFromIndex = (index: number): string | null => {
|
||||||
return addTracker(item);
|
const items = getTrackerItems();
|
||||||
};
|
if (index < 0 || index >= items.length) return null;
|
||||||
|
const item = items[index];
|
||||||
const removeTrackerItem = (emmet: string) => {
|
return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`;
|
||||||
return removeTracker(emmet);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTrackerItemByIndex = (index: number) => {
|
const removeTrackerItemByIndex = (index: number) => {
|
||||||
const items = trackerItems();
|
const emmet = getEmmetFromIndex(index);
|
||||||
if (index >= 0 && index < items.length) {
|
if (emmet) removeTrackerItem(emmet);
|
||||||
const item = items[index];
|
|
||||||
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
|
|
||||||
removeTracker(emmet);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerAttribute = (emmet: string, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
return updateTrackerAttr(emmet, attrName, attr);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
|
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
|
||||||
const items = trackerItems();
|
const emmet = getEmmetFromIndex(index);
|
||||||
if (index >= 0 && index < items.length) {
|
if (emmet) updateTrackerAttribute(emmet, attrName, attr);
|
||||||
const item = items[index];
|
|
||||||
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
|
|
||||||
updateTrackerAttr(emmet, attrName, attr);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
|
const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
|
||||||
const items = trackerItems();
|
const emmet = getEmmetFromIndex(index);
|
||||||
if (index >= 0 && index < items.length) {
|
if (emmet) updateTrackerClasses(emmet, classes);
|
||||||
const item = items[index];
|
|
||||||
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
|
|
||||||
updateTrackerClassesStore(emmet, classes);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
|
const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => {
|
||||||
return moveTracker(emmet, direction);
|
const emmet = getEmmetFromIndex(index);
|
||||||
};
|
if (emmet) moveTrackerItem(emmet, direction);
|
||||||
|
|
||||||
const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => {
|
|
||||||
const items = trackerItems();
|
|
||||||
if (index >= 0 && index < items.length) {
|
|
||||||
const item = items[index];
|
|
||||||
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
|
|
||||||
moveTracker(emmet, direction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrackerItemClass = (emmet: string, className: string) => {
|
|
||||||
return removeClassFromTracker(emmet, className);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTrackerItemClassByIndex = (index: number, className: string) => {
|
const removeTrackerItemClassByIndex = (index: number, className: string) => {
|
||||||
const items = trackerItems();
|
const emmet = getEmmetFromIndex(index);
|
||||||
if (index >= 0 && index < items.length) {
|
if (emmet) removeTrackerClass(emmet, className);
|
||||||
const item = items[index];
|
|
||||||
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
|
|
||||||
removeClassFromTracker(emmet, className);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackerItems = () => getTrackerItems();
|
|
||||||
const trackerHistory = () => getTrackerHistory();
|
|
||||||
|
|
||||||
const setTrackerItems = (updater: (prev: TrackerItem[]) => TrackerItem[]) => {
|
|
||||||
// 直接操作 store
|
|
||||||
const current = getTrackerItems();
|
|
||||||
const updated = updater(current);
|
|
||||||
// 计算差异并记录
|
|
||||||
if (updated.length !== current.length) {
|
|
||||||
// 长度变化,可能是批量操作
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const recordTrackerCommand = (cmd: Omit<TrackerCommand, 'timestamp'>) => {
|
|
||||||
// store 已经自动记录
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -524,7 +250,6 @@ export function useCommander(
|
||||||
selectedCompletion,
|
selectedCompletion,
|
||||||
isFocused,
|
isFocused,
|
||||||
setInputValue,
|
setInputValue,
|
||||||
setEntries,
|
|
||||||
setShowCompletions,
|
setShowCompletions,
|
||||||
setSelectedCompletion,
|
setSelectedCompletion,
|
||||||
setIsFocused,
|
setIsFocused,
|
||||||
|
|
@ -532,15 +257,15 @@ export function useCommander(
|
||||||
updateCompletions,
|
updateCompletions,
|
||||||
acceptCompletion,
|
acceptCompletion,
|
||||||
commands,
|
commands,
|
||||||
|
setCommands,
|
||||||
historyIndex,
|
historyIndex,
|
||||||
setHistoryIndex,
|
setHistoryIndex,
|
||||||
commandHistory,
|
commandHistory,
|
||||||
navigateHistory,
|
navigateHistory,
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
trackerItems,
|
trackerItems: getTrackerItems,
|
||||||
setTrackerItems,
|
trackerHistory: getTrackerHistory,
|
||||||
trackerHistory,
|
|
||||||
addTrackerItem,
|
addTrackerItem,
|
||||||
removeTrackerItem,
|
removeTrackerItem,
|
||||||
removeTrackerItemByIndex,
|
removeTrackerItemByIndex,
|
||||||
|
|
@ -549,8 +274,7 @@ export function useCommander(
|
||||||
updateTrackerClassesByIndex,
|
updateTrackerClassesByIndex,
|
||||||
moveTrackerItem,
|
moveTrackerItem,
|
||||||
moveTrackerItemByIndex,
|
moveTrackerItemByIndex,
|
||||||
removeTrackerItemClass,
|
removeTrackerItemClass: removeTrackerClass,
|
||||||
removeTrackerItemClassByIndex,
|
removeTrackerItemClassByIndex,
|
||||||
recordTrackerCommand,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,43 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
import { onMount, onCleanup, Show } from "solid-js";
|
import { onMount, onCleanup, createResource } from "solid-js";
|
||||||
import { useCommander } from "./hooks";
|
import { useCommander } from "./hooks";
|
||||||
import { CommanderInput } from "./CommanderInput";
|
import { CommanderInput } from "./CommanderInput";
|
||||||
import { CommanderEntries } from "./CommanderEntries";
|
import { CommanderEntries } from "./CommanderEntries";
|
||||||
import { TrackerView } from "./TrackerView";
|
import { TrackerView } from "./TrackerView";
|
||||||
import { TabBar } from "./TabBar";
|
import { TabBar } from "./TabBar";
|
||||||
import type { MdCommanderProps, TrackerAttribute } from "./types";
|
import type { MdCommanderProps } from "./types";
|
||||||
|
import { loadElementSrc } from "../utils/path";
|
||||||
|
import { initializeCommands, loadCommandTemplatesFromCSV, getCommands, getCommandsLoading, getCommandsError } from "./stores";
|
||||||
|
|
||||||
customElement<MdCommanderProps>(
|
customElement<MdCommanderProps>(
|
||||||
"md-commander",
|
"md-commander",
|
||||||
{ placeholder: "", class: "", height: "" },
|
{ placeholder: "", class: "", height: "", commandTemplates: "" },
|
||||||
(props, { element }) => {
|
(props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
const { articlePath, rawSrc } = loadElementSrc(element as any);
|
||||||
|
|
||||||
const commander = useCommander(props.commands);
|
// 初始化命令
|
||||||
|
const commands = initializeCommands(props.commands);
|
||||||
|
const commander = useCommander(commands);
|
||||||
|
|
||||||
|
// 使用 createResource 加载 CSV 模板
|
||||||
|
const [templateData] = createResource(
|
||||||
|
() => (rawSrc ? { path: rawSrc, articlePath } : null),
|
||||||
|
async (paths) => {
|
||||||
|
await loadCommandTemplatesFromCSV(paths.path, paths.articlePath);
|
||||||
|
return getCommands();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 当模板加载完成后更新 commander 中的命令
|
||||||
|
createResource(
|
||||||
|
() => templateData(),
|
||||||
|
(loadedCommands) => {
|
||||||
|
if (loadedCommands) {
|
||||||
|
commander.setCommands(loadedCommands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (commander.showCompletions() && commander.completions().length > 0) {
|
if (commander.showCompletions() && commander.completions().length > 0) {
|
||||||
|
|
@ -42,7 +66,6 @@ customElement<MdCommanderProps>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 补全未打开时,使用上下键浏览历史
|
|
||||||
if (e.key === "ArrowUp" && !commander.showCompletions()) {
|
if (e.key === "ArrowUp" && !commander.showCompletions()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
commander.navigateHistory("up");
|
commander.navigateHistory("up");
|
||||||
|
|
@ -74,19 +97,24 @@ customElement<MdCommanderProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${props.class || ""}`}
|
class={`w-full flex flex-col border border-gray-300 rounded-lg overflow-hidden mb-4 ${props.class || ""}`}
|
||||||
style={{ height: heightStyle() }}
|
style={{ height: heightStyle() }}
|
||||||
>
|
>
|
||||||
{/* 标签页导航 */}
|
<TabBar mode={commander.viewMode} onModeChange={commander.setViewMode} />
|
||||||
<TabBar
|
|
||||||
mode={commander.viewMode}
|
|
||||||
onModeChange={commander.setViewMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 内容区域:历史或追踪 */}
|
<div class="flex flex-1 flex-col lg:flex-row">
|
||||||
<Show
|
{/* 历史视图 */}
|
||||||
when={commander.viewMode() === "history"}
|
<div class={`flex-1 flex-col min-h-0 lg:flex lg:w-1/2 lg:border-r lg:border-gray-300 ${commander.viewMode() !== "history" ? "hidden" : ""}`}>
|
||||||
fallback={
|
<CommanderEntries
|
||||||
|
entries={commander.entries}
|
||||||
|
loading={getCommandsLoading()}
|
||||||
|
error={getCommandsError()}
|
||||||
|
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 追踪视图 */}
|
||||||
|
<div class={`flex-1 flex-col min-h-0 lg:flex lg:w-1/2 lg:border-r lg:border-gray-300 ${commander.viewMode() !== "tracker" ? "hidden" : ""}`}>
|
||||||
<TrackerView
|
<TrackerView
|
||||||
items={commander.trackerItems}
|
items={commander.trackerItems}
|
||||||
onEditAttribute={(index, attrName, attr) =>
|
onEditAttribute={(index, attrName, attr) =>
|
||||||
|
|
@ -95,20 +123,16 @@ customElement<MdCommanderProps>(
|
||||||
onClassesChange={(index, classes) =>
|
onClassesChange={(index, classes) =>
|
||||||
commander.updateTrackerClassesByIndex(index, classes)
|
commander.updateTrackerClassesByIndex(index, classes)
|
||||||
}
|
}
|
||||||
onRemoveClass={commander.removeTrackerItemClassByIndex}
|
onRemoveClass={(index, className) =>
|
||||||
|
commander.removeTrackerItemClassByIndex(index, className)
|
||||||
|
}
|
||||||
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
|
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
|
||||||
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
|
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
|
||||||
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
|
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
|
||||||
/>
|
/>
|
||||||
}
|
</div>
|
||||||
>
|
</div>
|
||||||
<CommanderEntries
|
|
||||||
entries={commander.entries}
|
|
||||||
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 命令输入框 */}
|
|
||||||
<div class="relative border-t border-gray-300">
|
<div class="relative border-t border-gray-300">
|
||||||
<CommanderInput
|
<CommanderInput
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
||||||
|
import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
|
||||||
|
import {resolvePath} from "../../utils/path";
|
||||||
|
import {loadCSV} from "../../utils/csv-loader";
|
||||||
|
|
||||||
|
const defaultCommands: MdCommanderCommandMap = {
|
||||||
|
help: setupHelpCommand({}),
|
||||||
|
clear: clearCommand,
|
||||||
|
roll: rollCommand,
|
||||||
|
track: trackCommand,
|
||||||
|
untrack: untrackCommand,
|
||||||
|
list: listTrackCommand,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CommandsStoreState {
|
||||||
|
commands: MdCommanderCommandMap;
|
||||||
|
initialized: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [commandsStore, setCommandsStore] = createStore<CommandsStoreState>({
|
||||||
|
commands: { ...defaultCommands },
|
||||||
|
initialized: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化命令(包括 help 命令的动态更新)
|
||||||
|
*/
|
||||||
|
export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap {
|
||||||
|
const commands = { ...defaultCommands, ...customCommands };
|
||||||
|
// 更新 help 命令的命令列表
|
||||||
|
commands.help = setupHelpCommand(commands);
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册命令到 store
|
||||||
|
*/
|
||||||
|
export function registerCommands(customCommands?: MdCommanderCommandMap): void {
|
||||||
|
const commands = initializeCommands(customCommands);
|
||||||
|
setCommandsStore({ commands, initialized: true, loading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 store 获取命令
|
||||||
|
*/
|
||||||
|
export function getCommands(): MdCommanderCommandMap {
|
||||||
|
return commandsStore.commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查命令是否已初始化
|
||||||
|
*/
|
||||||
|
export function isCommandsInitialized(): boolean {
|
||||||
|
return commandsStore.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个命令
|
||||||
|
*/
|
||||||
|
export function getCommand(name: string): MdCommanderCommand | undefined {
|
||||||
|
return commandsStore.commands[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加载状态
|
||||||
|
*/
|
||||||
|
export function getCommandsLoading(): boolean {
|
||||||
|
return commandsStore.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误信息
|
||||||
|
*/
|
||||||
|
export function getCommandsError(): string | undefined {
|
||||||
|
return commandsStore.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置加载状态
|
||||||
|
*/
|
||||||
|
export function setCommandsLoading(loading: boolean): void {
|
||||||
|
setCommandsStore("loading", loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置错误信息
|
||||||
|
*/
|
||||||
|
export function setCommandsError(error?: string): void {
|
||||||
|
setCommandsStore("error", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新命令
|
||||||
|
*/
|
||||||
|
export function updateCommands(updater: (prev: MdCommanderCommandMap) => MdCommanderCommandMap): void {
|
||||||
|
setCommandsStore("commands", (prev) => updater(prev));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 CSV 文件加载命令模板并更新命令定义
|
||||||
|
*/
|
||||||
|
export async function loadCommandTemplatesFromCSV(
|
||||||
|
path: string,
|
||||||
|
articlePath: string
|
||||||
|
): Promise<void> {
|
||||||
|
setCommandsLoading(true);
|
||||||
|
setCommandsError(undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csv = await loadCSV<CommandTemplateRow>(resolvePath(articlePath, path));
|
||||||
|
|
||||||
|
// 按命令分组模板
|
||||||
|
const templatesByCommand = new Map<string, CommandTemplateRow[]>();
|
||||||
|
for (const row of csv) {
|
||||||
|
if (!row.command || !row.label || !row.insertedText) continue;
|
||||||
|
|
||||||
|
if (!templatesByCommand.has(row.command)) {
|
||||||
|
templatesByCommand.set(row.command, []);
|
||||||
|
}
|
||||||
|
templatesByCommand.get(row.command)!.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个命令添加模板
|
||||||
|
updateCommands((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
for (const [commandName, rows] of templatesByCommand.entries()) {
|
||||||
|
const cmd = updated[commandName];
|
||||||
|
if (!cmd || !cmd.parameters) continue;
|
||||||
|
|
||||||
|
const templates = rows.map((row) => ({
|
||||||
|
label: row.label,
|
||||||
|
description: row.description || "",
|
||||||
|
insertText: row.insertedText,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 为每个参数添加模板
|
||||||
|
updated[commandName] = {
|
||||||
|
...cmd,
|
||||||
|
parameters: cmd.parameters.map((param) => ({
|
||||||
|
...param,
|
||||||
|
templates: param.templates ? [...param.templates, ...templates] : templates,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
setCommandsStore("initialized", true);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
setCommandsError(`加载命令模板失败:${errorMessage}`);
|
||||||
|
console.warn(`Error loading command templates from ${path}:`, error);
|
||||||
|
} finally {
|
||||||
|
setCommandsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandTemplateRow {
|
||||||
|
command: string;
|
||||||
|
parameter: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
insertedText: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import type { CommanderEntry } from "../types";
|
||||||
|
|
||||||
|
export interface EntriesStore {
|
||||||
|
entries: CommanderEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entriesStore, setEntriesStore] = createStore<EntriesStore>({
|
||||||
|
entries: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加命令执行记录
|
||||||
|
*/
|
||||||
|
export function addEntry(entry: CommanderEntry): void {
|
||||||
|
setEntriesStore("entries", (prev) => [...prev, entry]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有记录
|
||||||
|
*/
|
||||||
|
export function clearEntries(): void {
|
||||||
|
setEntriesStore("entries", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有记录
|
||||||
|
*/
|
||||||
|
export function getEntries(): CommanderEntry[] {
|
||||||
|
return entriesStore.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置记录
|
||||||
|
*/
|
||||||
|
export function setEntries(entries: CommanderEntry[]): void {
|
||||||
|
setEntriesStore("entries", entries);
|
||||||
|
}
|
||||||
|
|
@ -12,3 +12,26 @@ export {
|
||||||
findTrackerItem,
|
findTrackerItem,
|
||||||
} from "./trackerStore";
|
} from "./trackerStore";
|
||||||
export type { TrackerStore } from "./trackerStore";
|
export type { TrackerStore } from "./trackerStore";
|
||||||
|
|
||||||
|
export {
|
||||||
|
initializeCommands,
|
||||||
|
registerCommands,
|
||||||
|
getCommands,
|
||||||
|
isCommandsInitialized,
|
||||||
|
getCommand,
|
||||||
|
loadCommandTemplatesFromCSV,
|
||||||
|
getCommandsLoading,
|
||||||
|
getCommandsError,
|
||||||
|
setCommandsLoading,
|
||||||
|
setCommandsError,
|
||||||
|
updateCommands,
|
||||||
|
} from "./commandsStore";
|
||||||
|
export type { CommandsStoreState } from "./commandsStore";
|
||||||
|
|
||||||
|
export {
|
||||||
|
addEntry,
|
||||||
|
clearEntries,
|
||||||
|
getEntries,
|
||||||
|
setEntries,
|
||||||
|
} from "./entriesStore";
|
||||||
|
export type { EntriesStore } from "./entriesStore";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import type { Component } from "solid-js";
|
import type { Component } from "solid-js";
|
||||||
|
|
||||||
|
// ==================== 组件 Props ====================
|
||||||
|
|
||||||
export interface MdCommanderProps {
|
export interface MdCommanderProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
commands?: Record<string, MdCommanderCommand>;
|
commands?: Record<string, MdCommanderCommand>;
|
||||||
|
commandTemplates?: string | string[]; // CSV 文件路径或路径数组
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 命令定义 ====================
|
||||||
|
|
||||||
export interface MdCommanderCommandMap {
|
export interface MdCommanderCommandMap {
|
||||||
[key: string]: MdCommanderCommand;
|
[key: string]: MdCommanderCommand;
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +40,13 @@ export interface MdCommanderParameter {
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
values?: string[];
|
values?: string[];
|
||||||
|
templates?: MdCommanderTemplate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MdCommanderTemplate {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
insertText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MdCommanderOptionType =
|
export type MdCommanderOptionType =
|
||||||
|
|
@ -56,6 +68,8 @@ export interface MdCommanderOption {
|
||||||
EntryComponent?: Component<any>;
|
EntryComponent?: Component<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 命令执行记录 ====================
|
||||||
|
|
||||||
export interface CommanderEntry {
|
export interface CommanderEntry {
|
||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
|
|
@ -75,7 +89,9 @@ export interface CompletionItem {
|
||||||
insertText: string;
|
insertText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tracker 类型 ====================
|
// ==================== Tracker ====================
|
||||||
|
|
||||||
|
export type TrackerViewMode = "history" | "tracker";
|
||||||
|
|
||||||
export type TrackerAttributeType = "progress" | "count" | "string";
|
export type TrackerAttributeType = "progress" | "count" | "string";
|
||||||
|
|
||||||
|
|
@ -87,13 +103,11 @@ export interface TrackerAttribute {
|
||||||
|
|
||||||
export interface TrackerItem {
|
export interface TrackerItem {
|
||||||
tag: string;
|
tag: string;
|
||||||
id?: string; // Emmet ID (#id)
|
id?: string;
|
||||||
classes: string[];
|
classes: string[];
|
||||||
attributes: Record<string, TrackerAttribute>;
|
attributes: Record<string, TrackerAttribute>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrackerViewMode = "history" | "tracker";
|
|
||||||
|
|
||||||
export interface TrackerCommand {
|
export interface TrackerCommand {
|
||||||
type: "add" | "remove" | "update" | "reorder";
|
type: "add" | "remove" | "update" | "reorder";
|
||||||
itemId?: string;
|
itemId?: string;
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,14 @@ export function resolvePath(base: string, relative: string): string {
|
||||||
|
|
||||||
return '/' + baseParts.join('/');
|
return '/' + baseParts.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function loadElementSrc(element?: { textContent: string, closest: (arg0: string) => HTMLElement }){
|
||||||
|
const rawSrc = element?.textContent;
|
||||||
|
if(element) element.textContent = "";
|
||||||
|
|
||||||
|
const articleEl = element?.closest('article[data-src]');
|
||||||
|
const articlePath = articleEl?.getAttribute('data-src') || '';
|
||||||
|
|
||||||
|
return { articlePath, rawSrc };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue