Compare commits

..

No commits in common. "fa1c6d19e9a50cd027c98768071b2077bf9f0966" and "abaee79198cfea734da594fe6d06a8a643e761ba" have entirely different histories.

19 changed files with 508 additions and 715 deletions

12
QWEN.md
View File

@ -48,15 +48,3 @@ 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`。

View File

@ -1,3 +1,3 @@
# Commander Test # Commander Test
:md-commander[./commands.csv] :md-commander[]

View File

@ -1,4 +0,0 @@
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]"
1 command parameter label description insertedText
2 track emmet monk 破戒佛爷 破戒佛爷.bandit.monk[gd=4/4,str=14/14,cla=10,spi=8]
3 track emmet bandit 龙虎寨山贼 龙虎寨山贼.bandit[gd=2/2,str=10/10,cla=10,spi=10]
4 track emmet khitan 契丹护卫 契丹护卫.khitan.guard[gd=4/4,ap=2,str=14/14,cla=14,spi=14]

View File

@ -20,9 +20,9 @@ export type { TableProps } from './md-table';
export type { BgProps } from './md-bg'; export type { BgProps } from './md-bg';
// 导出 md-commander 相关 // 导出 md-commander 相关
export type { export type {
MdCommanderProps, MdCommanderProps,
MdCommanderCommand, MdCommanderCommand,
MdCommanderCommandMap, MdCommanderCommandMap,
MdCommanderParameter, MdCommanderParameter,
MdCommanderOption, MdCommanderOption,
@ -39,5 +39,4 @@ 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 } from './md-commander/hooks'; export { useCommander, defaultCommands } from './md-commander/hooks';
export { initializeCommands, getCommands } from './md-commander/stores/commandsStore';

View File

@ -1,17 +1,27 @@
import { type Component, For, Show } from "solid-js"; import { type Component, For, Show, createEffect, on } 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);
@ -24,54 +34,35 @@ export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
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 <Show
when={props.loading} when={props.entries().length > 0}
fallback={ fallback={
<Show <div class="text-gray-400 text-center py-8"></div>
when={props.error}
fallback={
<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 cursor-pointer hover:text-blue-600 hover:underline"
onClick={() => handleCommandClick(entry.command)}
title="点击复制到输入框"
>
{entry.command}
</span>
<span>
{entry.timestamp.toLocaleTimeString()}
</span>
</div>
<div
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
innerHTML={entry.result.isHtml ? entry.result.message : undefined}
>
{!entry.result.isHtml && entry.result.message}
</div>
</div>
)}
</For>
</Show>
}
>
<div class="text-red-500 text-center py-8">{props.error}</div>
</Show>
} }
> >
<div class="flex items-center justify-center h-full"> <For each={props.entries()}>
<div class="text-gray-500 flex items-center gap-2"> {(entry) => (
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div> <div class="border-l-2 border-gray-300 pl-3 py-1">
<span>...</span> <div class="flex items-center justify-between text-xs text-gray-500 mb-1">
</div> <span
</div> class="font-mono cursor-pointer hover:text-blue-600 hover:underline"
onClick={() => handleCommandClick(entry.command)}
title="点击复制到输入框"
>
{entry.command}
</span>
<span>
{entry.timestamp.toLocaleTimeString()}
</span>
</div>
<div
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
innerHTML={entry.result.isHtml ? entry.result.message : undefined}
>
{!entry.result.isHtml && entry.result.message}
</div>
</div>
)}
</For>
</Show> </Show>
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import {type Component, For, Show, createEffect, on} from "solid-js"; import {type Component, For, Show} from "solid-js";
import type { CompletionItem } from "./types"; import type { CompletionItem } from "./types";
export interface CommanderInputProps { export interface CommanderInputProps {
@ -17,25 +17,6 @@ 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>
@ -58,14 +39,9 @@ export const CommanderInput: Component<CommanderInputProps> = (props) => {
{/* 自动补全下拉框 - 向上弹出 */} {/* 自动补全下拉框 - 向上弹出 */}
<Show when={props.showCompletions() && props.completions().length > 0}> <Show when={props.showCompletions() && props.completions().length > 0}>
<div <div class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg mb-1">
ref={containerRef} <For each={props.completions().slice(0, 3)}>{(comp, idx) => (
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 <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"
@ -95,6 +71,11 @@ 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>

View File

@ -8,7 +8,7 @@ export interface TabBarProps {
export const TabBar: Component<TabBarProps> = (props) => { export const TabBar: Component<TabBarProps> = (props) => {
return ( return (
<div class="flex lg:hidden border-b border-gray-300 bg-gray-50"> <div class="flex 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"

View File

@ -34,10 +34,10 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
return `${name}=${val}`; return `${name}=${val}`;
}; };
const handleTagClick = (e: MouseEvent, index: number) => { const handleAttributeClick = (e: MouseEvent, index: number, attrName: string, attr: TrackerAttribute) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const rect = (e.target as HTMLElement).getBoundingClientRect();
setEditingItem({ setEditingItem({
index, index,
position: { position: {
@ -76,23 +76,32 @@ 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">
<button <span class="font-bold text-gray-800 whitespace-nowrap">{item.tag}</span>
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 text-gray-500 font-mono whitespace-nowrap truncate"> <span class="text-xs 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>

View File

@ -1,31 +1,37 @@
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types"; import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand { export const helpCommand: MdCommanderCommand = {
return { command: "help",
command: "help", description: "显示帮助信息或特定命令的帮助",
description: "显示帮助信息或特定命令的帮助", parameters: [
parameters: [ {
{ 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 {
message: `命令:${cmdName}\n描述${cmds[cmdName]?.description || "无描述"}`,
type: "info",
};
}
const cmdList = Object.keys(cmds || {}).filter((k) => k !== "help").join(", ");
return { return {
message: `可用命令:${cmdList}`, message: `命令:${cmdName}\n描述${commands[cmdName]?.description || "无描述"}`,
type: "info", type: "info",
}; };
}, }
}; const cmdList = Object.keys(commands || {}).filter(k => k !== "help").join(", ");
return {
message: `可用命令:${cmdList}`,
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;
} }

View File

@ -1,4 +1,4 @@
export { setupHelpCommand } from "./help"; export { helpCommand, 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";

View File

@ -1,182 +0,0 @@
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";
}
}

View File

@ -1,6 +1,13 @@
export { useCommander } from "./useCommander"; export { useCommander, defaultCommands, parseInput, getCompletions, getResultClass } from './useCommander';
export type { UseCommanderReturn } from "./useCommander"; export type { UseCommanderReturn } from './useCommander';
export { rollSimple, rollFormula } from "./useDiceRoller"; export { rollFormula, rollSimple } from './useDiceRoller';
export type { DiceRollerResult } from "./useDiceRoller"; export * from './dice-engine';
export { parseInput, getCompletions, getResultClass } from "./completions";
export type { ParsedCommand } from "./completions"; // Tracker 相关导出
export type {
TrackerItem,
TrackerAttribute,
TrackerAttributeType,
TrackerCommand,
TrackerViewMode,
} from '../types';

View File

@ -1,25 +1,226 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import type { import {
MdCommanderCommand,
MdCommanderCommandMap,
CommanderEntry, CommanderEntry,
CompletionItem, CompletionItem,
TrackerItem, TrackerItem,
TrackerAttribute, TrackerAttribute,
TrackerViewMode,
MdCommanderCommandMap,
TrackerCommand, TrackerCommand,
TrackerViewMode,
} from "../types"; } from "../types";
import { parseInput, getCompletions } from "./completions"; import { rollSimple } from "./useDiceRoller";
import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
import { import {
addTrackerItem, addTrackerItem as addTracker,
getTrackerHistory, removeTrackerItem as removeTracker,
updateTrackerAttribute as updateTrackerAttr,
updateTrackerClasses as updateTrackerClassesStore,
moveTrackerItem as moveTracker,
removeTrackerClass as removeClassFromTracker,
getTrackerItems, getTrackerItems,
moveTrackerItem, getTrackerHistory,
removeTrackerClass, findTrackerIndex,
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 ====================
@ -31,60 +232,71 @@ 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">) => TrackerItem; addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void;
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(initialCommands?: MdCommanderCommandMap): UseCommanderReturn { export function useCommander(
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 || {});
// 从 store 获取 entries const commands = { ...defaultCommands, ...customCommands };
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 cmds = commands(); const parsed = parseInput(input, commands);
const parsed = parseInput(input, cmds);
const commandName = parsed.command; const commandName = parsed.command;
const cmd = cmds[commandName!]; const cmd = commands[commandName!];
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean }; let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
@ -92,7 +304,7 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
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 }, cmds); result = cmd.handler({ params: parsed.params, options: parsed.options }, commands);
} catch (e) { } catch (e) {
result = { result = {
message: `执行错误:${e instanceof Error ? e.message : String(e)}`, message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
@ -111,31 +323,35 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
timestamp: new Date(), timestamp: new Date(),
}; };
// 使用 store 添加记录 setEntries((prev) => [...prev, newEntry]);
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") {
clearEntries(); setEntries([]);
} }
}; };
// ==================== 自动补全 ====================
const updateCompletions = () => { const updateCompletions = () => {
const comps = getCompletions(inputValue(), commands()); const input = inputValue();
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 maxIdx = completions().length - 1; const comps = completions();
if (typeof v === "function") { const maxVisible = 3;
setSelectedCompletionState((prev) => { const maxIdx = Math.min(comps.length - 1, maxVisible - 1);
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));
}); });
@ -147,33 +363,37 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
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 = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; newValue = `${base} ${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(" ");
newValue = `${parsed.command || ""} ${existingParams}${existingParams ? " " : ""}${comp.insertText}`; const existingOptions = Object.entries(parsed.options)
.map(([k, v]) => `--${k}=${v}`)
.join(" ");
newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`;
} else { } else {
newValue = input; newValue = input;
} }
@ -185,19 +405,22 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
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;
@ -205,41 +428,92 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
} }
setHistoryIndex(newIndex); setHistoryIndex(newIndex);
// 从历史末尾获取命令(最新的在前)
setInputValue(history[history.length - 1 - newIndex]); setInputValue(history[history.length - 1 - newIndex]);
}; };
// ==================== Tracker 操作 ==================== // ==================== Tracker 方法 ====================
const getEmmetFromIndex = (index: number): string | null => { const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => {
const items = getTrackerItems(); return addTracker(item);
if (index < 0 || index >= items.length) return null; };
const item = items[index];
return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`; const removeTrackerItem = (emmet: string) => {
return removeTracker(emmet);
}; };
const removeTrackerItemByIndex = (index: number) => { const removeTrackerItemByIndex = (index: number) => {
const emmet = getEmmetFromIndex(index); const items = trackerItems();
if (emmet) removeTrackerItem(emmet); 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('.') : ''}`;
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 emmet = getEmmetFromIndex(index); const items = trackerItems();
if (emmet) updateTrackerAttribute(emmet, attrName, attr); 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('.') : ''}`;
updateTrackerAttr(emmet, attrName, attr);
}
}; };
const updateTrackerClassesByIndex = (index: number, classes: string[]) => { const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
const emmet = getEmmetFromIndex(index); const items = trackerItems();
if (emmet) updateTrackerClasses(emmet, classes); 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('.') : ''}`;
updateTrackerClassesStore(emmet, classes);
}
}; };
const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => { const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
const emmet = getEmmetFromIndex(index); return moveTracker(emmet, direction);
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 emmet = getEmmetFromIndex(index); const items = trackerItems();
if (emmet) removeTrackerClass(emmet, className); 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('.') : ''}`;
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 {
@ -250,6 +524,7 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
selectedCompletion, selectedCompletion,
isFocused, isFocused,
setInputValue, setInputValue,
setEntries,
setShowCompletions, setShowCompletions,
setSelectedCompletion, setSelectedCompletion,
setIsFocused, setIsFocused,
@ -257,15 +532,15 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
updateCompletions, updateCompletions,
acceptCompletion, acceptCompletion,
commands, commands,
setCommands,
historyIndex, historyIndex,
setHistoryIndex, setHistoryIndex,
commandHistory, commandHistory,
navigateHistory, navigateHistory,
viewMode, viewMode,
setViewMode, setViewMode,
trackerItems: getTrackerItems, trackerItems,
trackerHistory: getTrackerHistory, setTrackerItems,
trackerHistory,
addTrackerItem, addTrackerItem,
removeTrackerItem, removeTrackerItem,
removeTrackerItemByIndex, removeTrackerItemByIndex,
@ -274,7 +549,8 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
updateTrackerClassesByIndex, updateTrackerClassesByIndex,
moveTrackerItem, moveTrackerItem,
moveTrackerItemByIndex, moveTrackerItemByIndex,
removeTrackerItemClass: removeTrackerClass, removeTrackerItemClass,
removeTrackerItemClassByIndex, removeTrackerItemClassByIndex,
recordTrackerCommand,
}; };
} }

View File

@ -1,43 +1,19 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import { onMount, onCleanup, createResource } from "solid-js"; import { onMount, onCleanup, Show } 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 } from "./types"; import type { MdCommanderProps, TrackerAttribute } 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: "", commandTemplates: "" }, { placeholder: "", class: "", height: "" },
(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) {
@ -66,6 +42,7 @@ customElement<MdCommanderProps>(
} }
} }
// 补全未打开时,使用上下键浏览历史
if (e.key === "ArrowUp" && !commander.showCompletions()) { if (e.key === "ArrowUp" && !commander.showCompletions()) {
e.preventDefault(); e.preventDefault();
commander.navigateHistory("up"); commander.navigateHistory("up");
@ -97,42 +74,41 @@ customElement<MdCommanderProps>(
return ( return (
<div <div
class={`w-full flex flex-col border border-gray-300 rounded-lg overflow-hidden mb-4 ${props.class || ""}`} class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${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
<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" : ""}`}> when={commander.viewMode() === "history"}
fallback={
<TrackerView
items={commander.trackerItems}
onEditAttribute={(index, attrName, attr) =>
commander.updateTrackerAttributeByIndex(index, attrName, attr)
}
onClassesChange={(index, classes) =>
commander.updateTrackerClassesByIndex(index, classes)
}
onRemoveClass={commander.removeTrackerItemClassByIndex}
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
/>
}
>
<CommanderEntries <CommanderEntries
entries={commander.entries} entries={commander.entries}
loading={getCommandsLoading()}
error={getCommandsError()}
onCommandClick={(cmd) => commander.setInputValue(cmd)} onCommandClick={(cmd) => commander.setInputValue(cmd)}
/> />
</div> </Show>
{/* 追踪视图 */}
<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
items={commander.trackerItems}
onEditAttribute={(index, attrName, attr) =>
commander.updateTrackerAttributeByIndex(index, attrName, attr)
}
onClassesChange={(index, classes) =>
commander.updateTrackerClassesByIndex(index, classes)
}
onRemoveClass={(index, className) =>
commander.removeTrackerItemClassByIndex(index, className)
}
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
/>
</div>
</div>
{/* 命令输入框 */}
<div class="relative border-t border-gray-300"> <div class="relative border-t border-gray-300">
<CommanderInput <CommanderInput
placeholder={props.placeholder} placeholder={props.placeholder}

View File

@ -1,168 +0,0 @@
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;
}

View File

@ -1,38 +0,0 @@
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);
}

View File

@ -12,26 +12,3 @@ 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";

View File

@ -1,17 +1,12 @@
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;
} }
@ -40,13 +35,6 @@ 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 =
@ -68,8 +56,6 @@ export interface MdCommanderOption {
EntryComponent?: Component<any>; EntryComponent?: Component<any>;
} }
// ==================== 命令执行记录 ====================
export interface CommanderEntry { export interface CommanderEntry {
id: string; id: string;
command: string; command: string;
@ -89,9 +75,7 @@ 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";
@ -103,11 +87,13 @@ export interface TrackerAttribute {
export interface TrackerItem { export interface TrackerItem {
tag: string; tag: string;
id?: string; id?: string; // Emmet ID (#id)
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;

View File

@ -37,14 +37,3 @@ 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 };
}