Compare commits
No commits in common. "abaee79198cfea734da594fe6d06a8a643e761ba" and "8693b28c75c9e245515af80baab05f5306d24d17" have entirely different histories.
abaee79198
...
8693b28c75
|
|
@ -17,26 +17,4 @@ export { FileTreeNode, HeadingNode } from './FileTree';
|
||||||
// 导出数据类型
|
// 导出数据类型
|
||||||
export type { DiceProps } from './md-dice';
|
export type { DiceProps } from './md-dice';
|
||||||
export type { TableProps } from './md-table';
|
export type { TableProps } from './md-table';
|
||||||
export type { BgProps } from './md-bg';
|
export type { BgProps } from './md-bg';
|
||||||
|
|
||||||
// 导出 md-commander 相关
|
|
||||||
export type {
|
|
||||||
MdCommanderProps,
|
|
||||||
MdCommanderCommand,
|
|
||||||
MdCommanderCommandMap,
|
|
||||||
MdCommanderParameter,
|
|
||||||
MdCommanderOption,
|
|
||||||
MdCommanderOptionType,
|
|
||||||
CommanderEntry,
|
|
||||||
CompletionItem,
|
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerAttributeType,
|
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
|
||||||
} from './md-commander/types';
|
|
||||||
export { TabBar } from './md-commander/TabBar';
|
|
||||||
export { TrackerView } from './md-commander/TrackerView';
|
|
||||||
export { CommanderEntries } from './md-commander/CommanderEntries';
|
|
||||||
export { CommanderInput } from './md-commander/CommanderInput';
|
|
||||||
export { useCommander, defaultCommands } from './md-commander/hooks';
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
import { type Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js";
|
|
||||||
import type { TrackerAttribute } from "./types";
|
|
||||||
|
|
||||||
export interface AttributeTooltipProps {
|
|
||||||
position: { x: number; y: number };
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
classes: string[];
|
|
||||||
onUpdate: (attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
onClassesChange: (classes: string[]) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
|
|
||||||
const [position, setPosition] = createSignal(props.position);
|
|
||||||
const [newClass, setNewClass] = createSignal("");
|
|
||||||
|
|
||||||
// 点击外部关闭
|
|
||||||
createEffect(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('body') && !target.closest(".attribute-tooltip")) {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
onCleanup(() => document.removeEventListener("click", handleClickOutside));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理属性更新
|
|
||||||
const handleUpdate = (attrName: string, value: any) => {
|
|
||||||
const attr = props.attributes[attrName];
|
|
||||||
props.onUpdate(attrName, { ...attr, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加 class
|
|
||||||
const handleAddClass = () => {
|
|
||||||
const className = newClass().trim();
|
|
||||||
if (className && !props.classes.includes(className)) {
|
|
||||||
props.onClassesChange([...props.classes, className]);
|
|
||||||
setNewClass("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除 class
|
|
||||||
const handleRemoveClass = (e: MouseEvent, className: string) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
props.onClassesChange(props.classes.filter((c) => c !== className));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染不同类型的属性编辑器
|
|
||||||
const renderEditor = (attrName: string, attr: TrackerAttribute) => {
|
|
||||||
if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
const percentage = val.y > 0 ? (val.x / val.y) * 100 : 0;
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
{/* 进度条 */}
|
|
||||||
<div class="w-full h-4 bg-gray-200 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-200"
|
|
||||||
style={{ width: `${Math.min(100, percentage)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* 输入框 */}
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={val.x}
|
|
||||||
onChange={(e) => handleUpdate(attrName, { x: parseInt(e.target.value) || 0, y: val.y })}
|
|
||||||
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500">/</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={val.y}
|
|
||||||
onChange={(e) => handleUpdate(attrName, { x: val.x, y: parseInt(e.target.value) || 0 })}
|
|
||||||
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr.type === "count") {
|
|
||||||
const value = attr.value as number;
|
|
||||||
return (
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
|
|
||||||
onClick={() => handleUpdate(attrName, value - 1)}
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => handleUpdate(attrName, parseInt(e.target.value) || 0)}
|
|
||||||
class="w-12 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
|
|
||||||
onClick={() => handleUpdate(attrName, value + 1)}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr.type === "string") {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={attr.value as string}
|
|
||||||
onChange={(e) => handleUpdate(attrName, e.target.value)}
|
|
||||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span class="text-gray-500">未知类型</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="attribute-tooltip fixed z-50 bg-white border border-gray-300 rounded-lg shadow-xl p-3 min-w-[150px]"
|
|
||||||
style={{
|
|
||||||
left: `${position().x}px`,
|
|
||||||
top: `${position().y}px`,
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-2 pb-2 border-b border-gray-200">
|
|
||||||
<span class="font-bold text-gray-700 text-sm">编辑属性</span>
|
|
||||||
<button
|
|
||||||
class="text-gray-400 hover:text-gray-600 text-lg leading-none"
|
|
||||||
onClick={props.onClose}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
{/* Class 列表编辑器 */}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 font-medium">Classes</label>
|
|
||||||
<div class="flex flex-wrap gap-1 mb-1">
|
|
||||||
<For each={props.classes}>
|
|
||||||
{(className) => (
|
|
||||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">
|
|
||||||
.{className}
|
|
||||||
<button
|
|
||||||
class="text-blue-400 hover:text-blue-600 font-bold leading-none"
|
|
||||||
onClick={(e) => handleRemoveClass(e, className)}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newClass()}
|
|
||||||
onInput={(e) => setNewClass((e.target as HTMLInputElement).value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleAddClass();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="添加 class..."
|
|
||||||
class="flex-1 px-2 py-1 border border-gray-300 rounded text-sm"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded text-sm font-medium transition-colors"
|
|
||||||
onClick={handleAddClass}
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 属性编辑器 */}
|
|
||||||
<For each={Object.entries(props.attributes)}>
|
|
||||||
{([name, attr]) => (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 font-medium">{name}</label>
|
|
||||||
{renderEditor(name, attr)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -39,8 +39,8 @@ 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">
|
<div class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg max-h-48 overflow-auto mb-1">
|
||||||
<For each={props.completions().slice(0, 3)}>{(comp, idx) => (
|
<For each={props.completions()}>{(comp, idx) => (
|
||||||
<div
|
<div
|
||||||
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()
|
||||||
|
|
@ -71,11 +71,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>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { type Component } from "solid-js";
|
|
||||||
import type { TrackerViewMode } from "./types";
|
|
||||||
|
|
||||||
export interface TabBarProps {
|
|
||||||
mode: () => TrackerViewMode;
|
|
||||||
onModeChange: (mode: TrackerViewMode) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TabBar: Component<TabBarProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<div class="flex border-b border-gray-300 bg-gray-50">
|
|
||||||
<button
|
|
||||||
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
props.mode() === "history"
|
|
||||||
? "bg-white text-blue-600 border-b-2 border-blue-600"
|
|
||||||
: "text-gray-600 hover:text-gray-800 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => props.onModeChange("history")}
|
|
||||||
>
|
|
||||||
📜 历史
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
props.mode() === "tracker"
|
|
||||||
? "bg-white text-blue-600 border-b-2 border-blue-600"
|
|
||||||
: "text-gray-600 hover:text-gray-800 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => props.onModeChange("tracker")}
|
|
||||||
>
|
|
||||||
📋 追踪
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
import { type Component, For, Show, createSignal } from "solid-js";
|
|
||||||
import type { TrackerItem, TrackerAttribute } from "./types";
|
|
||||||
import { AttributeTooltip } from "./AttributeTooltip";
|
|
||||||
|
|
||||||
export interface TrackerViewProps {
|
|
||||||
items: () => TrackerItem[];
|
|
||||||
onEditAttribute?: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
onClassesChange?: (index: number, classes: string[]) => void;
|
|
||||||
onRemoveClass?: (index: number, className: string) => void;
|
|
||||||
onMoveUp?: (index: number) => void;
|
|
||||||
onMoveDown?: (index: number) => void;
|
|
||||||
onRemove?: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TrackerView: Component<TrackerViewProps> = (props) => {
|
|
||||||
const [editingItem, setEditingItem] = createSignal<{
|
|
||||||
index: number;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const formatAttributeValue = (attr: TrackerAttribute): string => {
|
|
||||||
if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
return `${val.x}/${val.y}`;
|
|
||||||
}
|
|
||||||
if (attr.type === "count") {
|
|
||||||
return String(attr.value);
|
|
||||||
}
|
|
||||||
return String(attr.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAttributeShort = (name: string, attr: TrackerAttribute): string => {
|
|
||||||
const val = formatAttributeValue(attr);
|
|
||||||
return `${name}=${val}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAttributeClick = (e: MouseEvent, index: number, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
|
||||||
setEditingItem({
|
|
||||||
index,
|
|
||||||
position: {
|
|
||||||
x: rect.left,
|
|
||||||
y: rect.bottom + 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateAttribute = (attrName: string, attr: TrackerAttribute) => {
|
|
||||||
const data = editingItem();
|
|
||||||
if (data) {
|
|
||||||
props.onEditAttribute?.(data.index, attrName, attr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClassesChange = (classes: string[]) => {
|
|
||||||
const data = editingItem();
|
|
||||||
if (data) {
|
|
||||||
props.onClassesChange?.(data.index, classes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="tracker-view flex-1 overflow-auto p-3 bg-white">
|
|
||||||
<Show
|
|
||||||
when={props.items().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="text-gray-400 text-center py-8">暂无追踪项目</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<For each={props.items()}>
|
|
||||||
{(item, index) => (
|
|
||||||
<div class="border border-gray-200 rounded-lg p-2 bg-gray-50">
|
|
||||||
{/* 第一行:tag#id.class + 操作按钮 */}
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
|
||||||
<span class="font-bold text-gray-800 whitespace-nowrap">{item.tag}</span>
|
|
||||||
<Show when={item.id}>
|
|
||||||
<span class="text-xs text-purple-600 font-mono whitespace-nowrap">#{item.id}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={item.classes.length > 0}>
|
|
||||||
<span class="text-xs text-blue-600 font-mono whitespace-nowrap">.{item.classes.join(".")}</span>
|
|
||||||
</Show>
|
|
||||||
{/* 属性链接 - 可点击编辑 */}
|
|
||||||
<Show when={Object.keys(item.attributes).length > 0}>
|
|
||||||
<span class="text-xs font-mono whitespace-nowrap truncate">
|
|
||||||
[
|
|
||||||
<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>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 flex-shrink-0 ml-2">
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded text-xs"
|
|
||||||
title="上移"
|
|
||||||
onClick={() => props.onMoveUp?.(index())}
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded text-xs"
|
|
||||||
title="下移"
|
|
||||||
onClick={() => props.onMoveDown?.(index())}
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded text-xs"
|
|
||||||
title="移除"
|
|
||||||
onClick={() => props.onRemove?.(index())}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 属性编辑工具提示 */}
|
|
||||||
<Show when={editingItem()}>
|
|
||||||
<AttributeTooltip
|
|
||||||
position={editingItem()!.position}
|
|
||||||
attributes={props.items()[editingItem()!.index]?.attributes || {}}
|
|
||||||
classes={props.items()[editingItem()!.index]?.classes || []}
|
|
||||||
onUpdate={handleUpdateAttribute}
|
|
||||||
onClassesChange={handleClassesChange}
|
|
||||||
onClose={() => setEditingItem(null)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
|
|
||||||
export const clearCommand: MdCommanderCommand = {
|
|
||||||
command: "clear",
|
|
||||||
description: "清空命令历史",
|
|
||||||
handler: () => ({
|
|
||||||
message: "命令历史已清空",
|
|
||||||
type: "success",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
|
||||||
|
|
||||||
export const helpCommand: MdCommanderCommand = {
|
|
||||||
command: "help",
|
|
||||||
description: "显示帮助信息或特定命令的帮助",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "cmd",
|
|
||||||
description: "要查询的命令名",
|
|
||||||
type: "enum",
|
|
||||||
values: [], // 运行时填充
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args, commands) => {
|
|
||||||
const cmdName = args.params.cmd;
|
|
||||||
if (cmdName && commands?.[cmdName]) {
|
|
||||||
return {
|
|
||||||
message: `命令:${cmdName}\n描述:${commands[cmdName]?.description || "无描述"}`,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export { helpCommand, setupHelpCommand } from "./help";
|
|
||||||
export { clearCommand } from "./clear";
|
|
||||||
export { rollCommand } from "./roll";
|
|
||||||
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
import { rollSimple } from "../hooks/useDiceRoller";
|
|
||||||
|
|
||||||
export const rollCommand: MdCommanderCommand = {
|
|
||||||
command: "roll",
|
|
||||||
description: "掷骰子 - 支持骰池、修饰符和组合",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "formula",
|
|
||||||
description: "骰子公式,如 3d6+{d4,d8}kh2-5",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args) => {
|
|
||||||
const formula = args.params.formula || "1d6";
|
|
||||||
const result = rollSimple(formula);
|
|
||||||
return {
|
|
||||||
message: result.text,
|
|
||||||
isHtml: result.isHtml,
|
|
||||||
type: result.text.startsWith("错误") ? "error" : "success",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
import { parseEmmet } from "../utils";
|
|
||||||
import { addTrackerItem, removeTrackerItem, getTrackerItems, findTrackerItem } from "../stores";
|
|
||||||
|
|
||||||
export const trackCommand: MdCommanderCommand = {
|
|
||||||
command: "track",
|
|
||||||
description: "添加一个新的追踪项目 - 支持 Emmet 语法:track npc#john.dwarf.warrior[hp=4/4 ac=15]",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "emmet",
|
|
||||||
description: "Emmet 格式的追踪项目:tag#class1.class2[attr=value]",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args, commands) => {
|
|
||||||
const emmet = args.params.emmet;
|
|
||||||
if (!emmet) {
|
|
||||||
return { message: "错误:缺少 Emmet 参数", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseEmmet(emmet);
|
|
||||||
|
|
||||||
if (!parsed.tag) {
|
|
||||||
return { message: "错误:缺少 tag", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接添加追踪项目
|
|
||||||
addTrackerItem(parsed);
|
|
||||||
|
|
||||||
const idStr = parsed.id ? ` #${parsed.id}` : "";
|
|
||||||
const classStr = parsed.classes.length > 0 ? `.${parsed.classes.join(".")}` : "";
|
|
||||||
const attrCount = Object.keys(parsed.attributes).length;
|
|
||||||
const attrStr = attrCount > 0 ? `[${attrCount} 个属性]` : "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `已添加追踪项目:${parsed.tag}${idStr}${classStr}${attrStr}`,
|
|
||||||
type: "success",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const untrackCommand: MdCommanderCommand = {
|
|
||||||
command: "untrack",
|
|
||||||
description: "移除一个追踪项目 - 支持 Emmet 语法:untrack #g2 或 untrack goblin.warrior",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "emmet",
|
|
||||||
description: "Emmet 格式:#id 或 tag.class",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args) => {
|
|
||||||
const emmet = args.params.emmet;
|
|
||||||
if (!emmet) {
|
|
||||||
return { message: "错误:缺少 Emmet 参数", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findTrackerItem(emmet);
|
|
||||||
if (!found) {
|
|
||||||
return { message: `未找到匹配的追踪项目:${emmet}`, type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = removeTrackerItem(emmet);
|
|
||||||
if (!success) {
|
|
||||||
return { message: `移除失败:${emmet}`, type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: `已移除追踪项目:${found.tag}${found.id ? '#' + found.id : ''}`, type: "success" };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listTrackCommand: MdCommanderCommand = {
|
|
||||||
command: "list",
|
|
||||||
description: "列出所有追踪项目",
|
|
||||||
handler: (args, commands) => {
|
|
||||||
const items = getTrackerItems();
|
|
||||||
if (items.length === 0) {
|
|
||||||
return { message: "暂无追踪项目", type: "info" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = items.map((i) => `${i.tag}${i.id ? "#" + i.id : ""}${i.classes.length > 0 ? "." + i.classes.join(".") : ""}`).join("\n");
|
|
||||||
return { message: `追踪项目:\n${list}`, type: "info" };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -2,12 +2,3 @@ export { useCommander, defaultCommands, parseInput, getCompletions, getResultCla
|
||||||
export type { UseCommanderReturn } from './useCommander';
|
export type { UseCommanderReturn } from './useCommander';
|
||||||
export { rollFormula, rollSimple } from './useDiceRoller';
|
export { rollFormula, rollSimple } from './useDiceRoller';
|
||||||
export * from './dice-engine';
|
export * from './dice-engine';
|
||||||
|
|
||||||
// Tracker 相关导出
|
|
||||||
export type {
|
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerAttributeType,
|
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
|
||||||
} from '../types';
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,75 @@
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import {
|
import {
|
||||||
MdCommanderCommand,
|
MdCommanderCommand,
|
||||||
MdCommanderCommandMap,
|
|
||||||
CommanderEntry,
|
CommanderEntry,
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { rollSimple } from "./useDiceRoller";
|
import { rollSimple } from "./useDiceRoller";
|
||||||
import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
|
|
||||||
import {
|
|
||||||
addTrackerItem as addTracker,
|
|
||||||
removeTrackerItem as removeTracker,
|
|
||||||
updateTrackerAttribute as updateTrackerAttr,
|
|
||||||
updateTrackerClasses as updateTrackerClassesStore,
|
|
||||||
moveTrackerItem as moveTracker,
|
|
||||||
removeTrackerClass as removeClassFromTracker,
|
|
||||||
getTrackerItems,
|
|
||||||
getTrackerHistory,
|
|
||||||
findTrackerIndex,
|
|
||||||
} from "../stores";
|
|
||||||
|
|
||||||
// ==================== 默认命令 ====================
|
// ==================== 默认命令 ====================
|
||||||
|
|
||||||
export const defaultCommands: MdCommanderCommandMap = {
|
export const defaultCommands: Record<string, MdCommanderCommand> = {
|
||||||
help: setupHelpCommand({}),
|
help: {
|
||||||
clear: clearCommand,
|
command: "help",
|
||||||
roll: rollCommand,
|
description: "显示帮助信息或特定命令的帮助",
|
||||||
track: trackCommand,
|
parameters: [
|
||||||
untrack: untrackCommand,
|
{
|
||||||
list: listTrackCommand,
|
name: "cmd",
|
||||||
|
description: "要查询的命令名",
|
||||||
|
type: "enum",
|
||||||
|
values: [], // 运行时填充
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: (args) => {
|
||||||
|
const cmdName = args.params.cmd;
|
||||||
|
if (cmdName) {
|
||||||
|
return {
|
||||||
|
message: `命令:${cmdName}\n描述:${defaultCommands[cmdName]?.description || "无描述"}`,
|
||||||
|
type: "info",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const cmdList = Object.keys(defaultCommands).join(", ");
|
||||||
|
return {
|
||||||
|
message: `可用命令:${cmdList}`,
|
||||||
|
type: "info",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
command: "clear",
|
||||||
|
description: "清空命令历史",
|
||||||
|
handler: () => ({
|
||||||
|
message: "命令历史已清空",
|
||||||
|
type: "success",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
roll: {
|
||||||
|
command: "roll",
|
||||||
|
description: "掷骰子 - 支持骰池、修饰符和组合",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "formula",
|
||||||
|
description: "骰子公式,如 3d6+{d4,d8}kh2-5",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: (args) => {
|
||||||
|
const formula = args.params.formula || "1d6";
|
||||||
|
const result = rollSimple(formula);
|
||||||
|
return {
|
||||||
|
message: result.text,
|
||||||
|
isHtml: result.isHtml,
|
||||||
|
type: result.text.startsWith("错误") ? "error" : "success",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化默认命令
|
|
||||||
Object.keys(defaultCommands).forEach(key => {
|
|
||||||
if (key === "help") {
|
|
||||||
const help = setupHelpCommand(defaultCommands);
|
|
||||||
defaultCommands.help = help;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
export function parseInput(input: string, commands?: MdCommanderCommandMap): {
|
export function parseInput(input: string, commands?: Record<string, MdCommanderCommand>): {
|
||||||
command?: string;
|
command?: string;
|
||||||
params: Record<string, string>;
|
params: Record<string, string>;
|
||||||
options: Record<string, string>;
|
options: Record<string, string>;
|
||||||
|
|
@ -131,7 +156,7 @@ export function parseInput(input: string, commands?: MdCommanderCommandMap): {
|
||||||
|
|
||||||
export function getCompletions(
|
export function getCompletions(
|
||||||
input: string,
|
input: string,
|
||||||
commands: MdCommanderCommandMap,
|
commands: Record<string, MdCommanderCommand>,
|
||||||
): CompletionItem[] {
|
): CompletionItem[] {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
|
|
||||||
|
|
@ -239,48 +264,27 @@ export interface UseCommanderReturn {
|
||||||
handleCommand: () => void;
|
handleCommand: () => void;
|
||||||
updateCompletions: () => void;
|
updateCompletions: () => void;
|
||||||
acceptCompletion: () => void;
|
acceptCompletion: () => void;
|
||||||
commands: MdCommanderCommandMap;
|
commands: Record<string, MdCommanderCommand>;
|
||||||
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 相关
|
|
||||||
viewMode: () => TrackerViewMode;
|
|
||||||
setViewMode: (mode: TrackerViewMode) => void;
|
|
||||||
trackerItems: () => TrackerItem[];
|
|
||||||
setTrackerItems: (updater: (prev: TrackerItem[]) => TrackerItem[]) => void;
|
|
||||||
trackerHistory: () => TrackerCommand[];
|
|
||||||
addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void;
|
|
||||||
removeTrackerItem: (emmet: string) => boolean;
|
|
||||||
removeTrackerItemByIndex: (index: number) => void;
|
|
||||||
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
|
|
||||||
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
|
|
||||||
moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean;
|
|
||||||
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void;
|
|
||||||
removeTrackerItemClass: (emmet: string, className: string) => boolean;
|
|
||||||
removeTrackerItemClassByIndex: (index: number, className: string) => void;
|
|
||||||
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommander(
|
export function useCommander(
|
||||||
customCommands?: MdCommanderCommandMap,
|
customCommands?: Record<string, MdCommanderCommand>,
|
||||||
): UseCommanderReturn {
|
): UseCommanderReturn {
|
||||||
const [inputValue, setInputValue] = createSignal("");
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
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, setSelectedCompletion] = 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 commands = { ...defaultCommands, ...customCommands };
|
const commands = { ...defaultCommands, ...customCommands };
|
||||||
|
|
||||||
// 更新 help 命令的参数值
|
// 更新 help 命令的参数值
|
||||||
|
|
@ -304,7 +308,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 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = {
|
result = {
|
||||||
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
|
@ -324,11 +328,11 @@ export function useCommander(
|
||||||
};
|
};
|
||||||
|
|
||||||
setEntries((prev) => [...prev, newEntry]);
|
setEntries((prev) => [...prev, newEntry]);
|
||||||
|
|
||||||
// 添加到命令历史
|
// 添加到命令历史
|
||||||
setCommandHistory((prev) => [...prev, input]);
|
setCommandHistory((prev) => [...prev, input]);
|
||||||
setHistoryIndex(-1); // 重置历史索引
|
setHistoryIndex(-1); // 重置历史索引
|
||||||
|
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setShowCompletions(false);
|
setShowCompletions(false);
|
||||||
|
|
||||||
|
|
@ -342,30 +346,12 @@ export function useCommander(
|
||||||
const comps = getCompletions(input, commands);
|
const comps = getCompletions(input, commands);
|
||||||
setCompletions(comps);
|
setCompletions(comps);
|
||||||
setShowCompletions(comps.length > 0 && isFocused());
|
setShowCompletions(comps.length > 0 && isFocused());
|
||||||
setSelectedCompletionState(0);
|
setSelectedCompletion(0);
|
||||||
};
|
|
||||||
|
|
||||||
const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
|
|
||||||
const comps = completions();
|
|
||||||
const maxVisible = 3;
|
|
||||||
const maxIdx = Math.min(comps.length - 1, maxVisible - 1);
|
|
||||||
|
|
||||||
if (typeof v === 'function') {
|
|
||||||
setSelectedCompletionState(prev => {
|
|
||||||
const next = v(prev);
|
|
||||||
return Math.max(0, Math.min(next, maxIdx));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedCompletionState(Math.max(0, Math.min(v, maxIdx)));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptCompletion = () => {
|
const acceptCompletion = () => {
|
||||||
const idx = selectedCompletion();
|
const idx = selectedCompletion();
|
||||||
const comps = completions();
|
const comp = completions()[idx];
|
||||||
// 确保索引在有效范围内
|
|
||||||
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();
|
||||||
|
|
@ -432,90 +418,6 @@ export function useCommander(
|
||||||
setInputValue(history[history.length - 1 - newIndex]);
|
setInputValue(history[history.length - 1 - newIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== Tracker 方法 ====================
|
|
||||||
|
|
||||||
const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => {
|
|
||||||
return addTracker(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrackerItem = (emmet: string) => {
|
|
||||||
return removeTracker(emmet);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrackerItemByIndex = (index: number) => {
|
|
||||||
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('.') : ''}`;
|
|
||||||
removeTracker(emmet);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerAttribute = (emmet: string, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
return updateTrackerAttr(emmet, attrName, attr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
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('.') : ''}`;
|
|
||||||
updateTrackerAttr(emmet, attrName, attr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
|
|
||||||
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('.') : ''}`;
|
|
||||||
updateTrackerClassesStore(emmet, classes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
|
|
||||||
return moveTracker(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 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('.') : ''}`;
|
|
||||||
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 {
|
||||||
inputValue,
|
inputValue,
|
||||||
entries,
|
entries,
|
||||||
|
|
@ -536,21 +438,5 @@ export function useCommander(
|
||||||
setHistoryIndex,
|
setHistoryIndex,
|
||||||
commandHistory,
|
commandHistory,
|
||||||
navigateHistory,
|
navigateHistory,
|
||||||
viewMode,
|
|
||||||
setViewMode,
|
|
||||||
trackerItems,
|
|
||||||
setTrackerItems,
|
|
||||||
trackerHistory,
|
|
||||||
addTrackerItem,
|
|
||||||
removeTrackerItem,
|
|
||||||
removeTrackerItemByIndex,
|
|
||||||
updateTrackerAttribute,
|
|
||||||
updateTrackerAttributeByIndex,
|
|
||||||
updateTrackerClassesByIndex,
|
|
||||||
moveTrackerItem,
|
|
||||||
moveTrackerItemByIndex,
|
|
||||||
removeTrackerItemClass,
|
|
||||||
removeTrackerItemClassByIndex,
|
|
||||||
recordTrackerCommand,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
import { onMount, onCleanup, Show } from "solid-js";
|
import { onMount, onCleanup } 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 type { MdCommanderProps } from "./types";
|
||||||
import { TabBar } from "./TabBar";
|
|
||||||
import type { MdCommanderProps, TrackerAttribute } from "./types";
|
|
||||||
|
|
||||||
customElement<MdCommanderProps>(
|
customElement<MdCommanderProps>(
|
||||||
"md-commander",
|
"md-commander",
|
||||||
|
|
@ -77,37 +75,12 @@ customElement<MdCommanderProps>(
|
||||||
class={`md-commander flex flex-col border border-gray-300 rounded-lg overflow-hidden ${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
|
<CommanderEntries
|
||||||
mode={commander.viewMode}
|
entries={commander.entries}
|
||||||
onModeChange={commander.setViewMode}
|
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 内容区域:历史或追踪 */}
|
|
||||||
<Show
|
|
||||||
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
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
export {
|
|
||||||
addTrackerItem,
|
|
||||||
removeTrackerItem,
|
|
||||||
updateTrackerAttribute,
|
|
||||||
updateTrackerClasses,
|
|
||||||
moveTrackerItem,
|
|
||||||
removeTrackerClass,
|
|
||||||
getTrackerItems,
|
|
||||||
getTrackerHistory,
|
|
||||||
clearTrackerHistory,
|
|
||||||
findTrackerIndex,
|
|
||||||
findTrackerItem,
|
|
||||||
} from "./trackerStore";
|
|
||||||
export type { TrackerStore } from "./trackerStore";
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
import { createStore } from "solid-js/store";
|
|
||||||
import type { TrackerItem, TrackerAttribute, TrackerCommand } from "../types";
|
|
||||||
import {parseEmmet} from "../utils";
|
|
||||||
|
|
||||||
export interface TrackerStore {
|
|
||||||
items: TrackerItem[];
|
|
||||||
history: TrackerCommand[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tracker, setTracker] = createStore<TrackerStore>({
|
|
||||||
items: [],
|
|
||||||
history: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 Emmet 语法查找匹配的项目
|
|
||||||
* 支持:
|
|
||||||
* - #id - 匹配 id
|
|
||||||
* - tag - 匹配 tag
|
|
||||||
* - tag.class - 匹配 tag 和 class
|
|
||||||
* - .class - 匹配 class
|
|
||||||
* - [attr=value] - 匹配属性
|
|
||||||
*/
|
|
||||||
export function findTrackerIndex(emmet: string): number {
|
|
||||||
const {tag, id, classes, attributes} = parseEmmet(emmet);
|
|
||||||
|
|
||||||
return tracker.items.findIndex(item => {
|
|
||||||
// 匹配 tag
|
|
||||||
if (tag && item.tag !== tag) return false;
|
|
||||||
// 匹配 id
|
|
||||||
if (id && item.id !== id) return false;
|
|
||||||
// 匹配所有 classes
|
|
||||||
if (classes.length > 0) {
|
|
||||||
for (const cls of classes) {
|
|
||||||
if (!item.classes.includes(cls)) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 匹配属性
|
|
||||||
for(const attrName in attributes){
|
|
||||||
const attr = attributes[attrName];
|
|
||||||
if (item.attributes[attrName] !== attr) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findTrackerItem(emmet: string): TrackerItem | undefined {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
return index >= 0 ? tracker.items[index] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addTrackerItem(item: Omit<TrackerItem, "uuid">): TrackerItem {
|
|
||||||
const newItem: TrackerItem = {
|
|
||||||
...item,
|
|
||||||
};
|
|
||||||
|
|
||||||
setTracker("items", (prev) => [...prev, newItem]);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "add",
|
|
||||||
itemId: newItem.id,
|
|
||||||
data: newItem,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return newItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeTrackerItem(emmet: string): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
|
|
||||||
setTracker("items", (prev) => prev.filter((_, i) => i !== index));
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "remove",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
data: item,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackerAttribute(
|
|
||||||
emmet: string,
|
|
||||||
attrName: string,
|
|
||||||
attr: TrackerAttribute
|
|
||||||
): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
attributeUpdates: { [attrName]: attr },
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moveTrackerItem(emmet: string, direction: "up" | "down"): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
|
||||||
if (newIndex < 0 || newIndex >= tracker.items.length) return false;
|
|
||||||
|
|
||||||
const newItems = [...tracker.items];
|
|
||||||
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
|
|
||||||
|
|
||||||
setTracker("items", newItems);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "reorder",
|
|
||||||
data: newItems,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeTrackerClass(emmet: string, className: string): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, classes: i.classes.filter((c) => c !== className) }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackerClasses(emmet: string, classes: string[]): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, classes: [...classes] }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTrackerItems(): TrackerItem[] {
|
|
||||||
return tracker.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTrackerHistory(): TrackerCommand[] {
|
|
||||||
return tracker.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearTrackerHistory() {
|
|
||||||
setTracker("history", []);
|
|
||||||
}
|
|
||||||
|
|
@ -7,10 +7,6 @@ export interface MdCommanderProps {
|
||||||
commands?: Record<string, MdCommanderCommand>;
|
commands?: Record<string, MdCommanderCommand>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MdCommanderCommandMap {
|
|
||||||
[key: string]: MdCommanderCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MdCommanderCommand {
|
export interface MdCommanderCommand {
|
||||||
command: string;
|
command: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -19,7 +15,7 @@ export interface MdCommanderCommand {
|
||||||
handler?: (args: {
|
handler?: (args: {
|
||||||
params: Record<string, string>;
|
params: Record<string, string>;
|
||||||
options: Record<string, string>;
|
options: Record<string, string>;
|
||||||
}, commands?: MdCommanderCommandMap) => {
|
}) => {
|
||||||
message: string;
|
message: string;
|
||||||
type?: "success" | "error" | "info" | "warning";
|
type?: "success" | "error" | "info" | "warning";
|
||||||
isHtml?: boolean;
|
isHtml?: boolean;
|
||||||
|
|
@ -74,30 +70,3 @@ export interface CompletionItem {
|
||||||
description?: string;
|
description?: string;
|
||||||
insertText: string;
|
insertText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tracker 类型 ====================
|
|
||||||
|
|
||||||
export type TrackerAttributeType = "progress" | "count" | "string";
|
|
||||||
|
|
||||||
export interface TrackerAttribute {
|
|
||||||
name: string;
|
|
||||||
type: TrackerAttributeType;
|
|
||||||
value: string | number | { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackerItem {
|
|
||||||
tag: string;
|
|
||||||
id?: string; // Emmet ID (#id)
|
|
||||||
classes: string[];
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TrackerViewMode = "history" | "tracker";
|
|
||||||
|
|
||||||
export interface TrackerCommand {
|
|
||||||
type: "add" | "remove" | "update" | "reorder";
|
|
||||||
itemId?: string;
|
|
||||||
data?: Partial<TrackerItem> | TrackerItem[];
|
|
||||||
attributeUpdates?: Record<string, TrackerAttribute>;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import type { TrackerAttribute } from "../types";
|
|
||||||
|
|
||||||
export interface ParsedEmmet {
|
|
||||||
tag: string;
|
|
||||||
id?: string;
|
|
||||||
classes: string[];
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 Emmet 风格的 tracker 语法
|
|
||||||
* 格式:tag#id.class1.class2[attr1=value1 attr2=value2]
|
|
||||||
* 示例:npc#john.dwarf.warrior[hp=4/4 ac=15 name="John the Dwarf"]
|
|
||||||
*/
|
|
||||||
export function parseEmmet(input: string): ParsedEmmet {
|
|
||||||
const result: ParsedEmmet = {
|
|
||||||
tag: "",
|
|
||||||
classes: [],
|
|
||||||
attributes: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!input) return result;
|
|
||||||
|
|
||||||
// 匹配属性部分 [...]
|
|
||||||
const attrMatch = input.match(/\[(.+)\]$/);
|
|
||||||
let attrString: string | undefined;
|
|
||||||
let mainPart: string;
|
|
||||||
|
|
||||||
if (attrMatch) {
|
|
||||||
attrString = attrMatch[1];
|
|
||||||
mainPart = input.slice(0, attrMatch.index);
|
|
||||||
} else {
|
|
||||||
mainPart = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 tag、id 和 classes
|
|
||||||
// 格式:tag#id.class1.class2.class3
|
|
||||||
const tagClassMatch = mainPart.match(/^([^#.\s]+)?(?:#([^.\s]+))?(?:\.([^.]+(?:\.[^.]+)*))?/);
|
|
||||||
|
|
||||||
if (tagClassMatch) {
|
|
||||||
if (tagClassMatch[1]) {
|
|
||||||
result.tag = tagClassMatch[1];
|
|
||||||
}
|
|
||||||
if (tagClassMatch[2]) {
|
|
||||||
// # 后面的是 ID,不是 class
|
|
||||||
result.id = tagClassMatch[2];
|
|
||||||
}
|
|
||||||
if (tagClassMatch[3]) {
|
|
||||||
result.classes.push(...tagClassMatch[3].split("."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析属性
|
|
||||||
if (attrString) {
|
|
||||||
result.attributes = parseAttributes(attrString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析属性字符串
|
|
||||||
* 支持格式:
|
|
||||||
* - hp=4/4 (progress 类型,自动检测)
|
|
||||||
* - count=5 (count 类型,纯数字)
|
|
||||||
* - name="John" (string 类型,带引号)
|
|
||||||
* - name=John (string 类型,不带引号)
|
|
||||||
*/
|
|
||||||
function parseAttributes(attrString: string): Record<string, TrackerAttribute> {
|
|
||||||
const attributes: Record<string, TrackerAttribute> = {};
|
|
||||||
|
|
||||||
// 匹配键值对:使用,分隔
|
|
||||||
const regex = /(\w+)=(?:"([^"]*)"|'([^']*)'|([^,]+))/g;
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((match = regex.exec(attrString)) !== null) {
|
|
||||||
const key = match[1];
|
|
||||||
// 匹配的值可能是第 2、3 或 4 组(取决于引号类型)
|
|
||||||
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
||||||
|
|
||||||
// 自动检测类型
|
|
||||||
let attr: TrackerAttribute;
|
|
||||||
|
|
||||||
// 检查是否是 progress 格式 (x/y)
|
|
||||||
const progressMatch = value.match(/^(\d+)\/(\d+)$/);
|
|
||||||
if (progressMatch) {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "progress",
|
|
||||||
value: { x: parseInt(progressMatch[1]), y: parseInt(progressMatch[2]) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 检查是否是纯数字(count 类型)
|
|
||||||
else if (/^\d+$/.test(value)) {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "count",
|
|
||||||
value: parseInt(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 默认 string 类型
|
|
||||||
else {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "string",
|
|
||||||
value: value.replace(/^["']|["']$/g, ""), // 移除可能的引号
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes[key] = attr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { parseEmmet } from './emmetParser';
|
|
||||||
export type { ParsedEmmet } from './emmetParser';
|
|
||||||
27
todo.md
27
todo.md
|
|
@ -1,27 +0,0 @@
|
||||||
# todo
|
|
||||||
|
|
||||||
## md-commander(./components/md-commander)
|
|
||||||
|
|
||||||
- [x] create a new file for each command
|
|
||||||
- [x] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view.
|
|
||||||
- [x] add a command to update the tracker view.
|
|
||||||
|
|
||||||
the tracker view should show a list of currently tracked information.
|
|
||||||
|
|
||||||
each entry should have a tag, an id, a list of classes, and a list of attributes.
|
|
||||||
|
|
||||||
each attribute can be of the following types:
|
|
||||||
- progress: represented in x/y format, x y are both ints.
|
|
||||||
- count: represented as an int.
|
|
||||||
- string: represented as a string.
|
|
||||||
|
|
||||||
the tracker view should support the following interactions:
|
|
||||||
- reordering
|
|
||||||
- updating attributes, by using popup controls that appear when you click on the attribute
|
|
||||||
- removing classes
|
|
||||||
|
|
||||||
each interaction should be implemented as a command entry in the history view.
|
|
||||||
|
|
||||||
### 新增功能
|
|
||||||
|
|
||||||
- [x] 支持 Emmet 简写语法:`track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]`
|
|
||||||
Loading…
Reference in New Issue