feat: ai's attempt
This commit is contained in:
parent
8693b28c75
commit
f7eabbdeba
|
|
@ -18,3 +18,26 @@ 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 { AttributeEditor } from './md-commander/AttributeEditor';
|
||||||
|
export { CommanderEntries } from './md-commander/CommanderEntries';
|
||||||
|
export { CommanderInput } from './md-commander/CommanderInput';
|
||||||
|
export { useCommander, defaultCommands } from './md-commander/hooks';
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { type Component, Show } from "solid-js";
|
||||||
|
import type { TrackerAttribute, TrackerAttributeType } from "./types";
|
||||||
|
|
||||||
|
export interface AttributeEditorProps {
|
||||||
|
attribute: () => TrackerAttribute;
|
||||||
|
onUpdate: (attr: TrackerAttribute) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AttributeEditor: Component<AttributeEditorProps> = (props) => {
|
||||||
|
const updateValue = (value: any) => {
|
||||||
|
props.onUpdate({
|
||||||
|
...props.attribute(),
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEditor = () => {
|
||||||
|
const attr = props.attribute();
|
||||||
|
|
||||||
|
if (attr.type === "progress") {
|
||||||
|
const val = attr.value as { x: number; y: number };
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={val.x}
|
||||||
|
onChange={(e) => updateValue({ x: parseInt(e.target.value) || 0, y: val.y })}
|
||||||
|
class="w-16 px-2 py-1 border border-gray-300 rounded text-center"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<span>/</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={val.y}
|
||||||
|
onChange={(e) => updateValue({ x: val.x, y: parseInt(e.target.value) || 0 })}
|
||||||
|
class="w-16 px-2 py-1 border border-gray-300 rounded text-center"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr.type === "count") {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={attr.value as number}
|
||||||
|
onChange={(e) => updateValue(parseInt(e.target.value) || 0)}
|
||||||
|
class="w-24 px-2 py-1 border border-gray-300 rounded text-center"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr.type === "string") {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={attr.value as string}
|
||||||
|
onChange={(e) => updateValue(e.target.value)}
|
||||||
|
class="w-full px-2 py-1 border border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg p-4 min-w-[300px] shadow-xl">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-bold text-gray-800">编辑属性:{props.attribute().name}</h3>
|
||||||
|
<button
|
||||||
|
class="text-gray-500 hover:text-gray-700 text-xl"
|
||||||
|
onClick={props.onClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm text-gray-600 mb-1">类型</label>
|
||||||
|
<span class="inline-block px-2 py-1 bg-gray-100 rounded text-sm font-mono">
|
||||||
|
{props.attribute().type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm text-gray-600 mb-1">值</label>
|
||||||
|
{renderEditor()}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
onClick={props.onClose}
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { type Component, For, Show } from "solid-js";
|
||||||
|
import type { TrackerItem, TrackerAttribute } from "./types";
|
||||||
|
|
||||||
|
export interface TrackerViewProps {
|
||||||
|
items: () => TrackerItem[];
|
||||||
|
onEditAttribute?: (itemId: string, attrName: string, attr: TrackerAttribute) => void;
|
||||||
|
onRemoveClass?: (itemId: string, className: string) => void;
|
||||||
|
onMoveUp?: (itemId: string) => void;
|
||||||
|
onMoveDown?: (itemId: string) => void;
|
||||||
|
onRemove?: (itemId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrackerView: Component<TrackerViewProps> = (props) => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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-4">
|
||||||
|
<For each={props.items()}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="border border-gray-200 rounded-lg p-3 bg-gray-50">
|
||||||
|
{/* 头部:tag 和操作按钮 */}
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-bold text-gray-800">{item.tag}</span>
|
||||||
|
<span class="text-xs text-gray-500 font-mono">#{item.id.slice(-6)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"
|
||||||
|
title="上移"
|
||||||
|
onClick={() => props.onMoveUp?.(item.id)}
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"
|
||||||
|
title="下移"
|
||||||
|
onClick={() => props.onMoveDown?.(item.id)}
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded"
|
||||||
|
title="移除"
|
||||||
|
onClick={() => props.onRemove?.(item.id)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Classes */}
|
||||||
|
<Show when={item.classes.length > 0}>
|
||||||
|
<div class="flex flex-wrap gap-1 mb-2">
|
||||||
|
<For each={item.classes}>
|
||||||
|
{(cls) => (
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">
|
||||||
|
{cls}
|
||||||
|
<button
|
||||||
|
class="hover:text-red-600"
|
||||||
|
onClick={() => props.onRemoveClass?.(item.id, cls)}
|
||||||
|
title="移除类"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Attributes */}
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<For each={Object.values(item.attributes)}>
|
||||||
|
{(attr: TrackerAttribute) => (
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between px-2 py-1 bg-white rounded border border-gray-200 cursor-pointer hover:border-blue-400"
|
||||||
|
onClick={() => props.onEditAttribute?.(item.id, attr.name, attr)}
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-600">{attr.name}</span>
|
||||||
|
<span class="text-sm font-mono text-gray-800">
|
||||||
|
{formatAttributeValue(attr)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { MdCommanderCommand } from "../types";
|
||||||
|
|
||||||
|
export const clearCommand: MdCommanderCommand = {
|
||||||
|
command: "clear",
|
||||||
|
description: "清空命令历史",
|
||||||
|
handler: () => ({
|
||||||
|
message: "命令历史已清空",
|
||||||
|
type: "success",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { helpCommand, setupHelpCommand } from "./help";
|
||||||
|
export { clearCommand } from "./clear";
|
||||||
|
export { rollCommand } from "./roll";
|
||||||
|
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import type { MdCommanderCommand, MdCommanderCommandMap, TrackerAttributeType } from "../types";
|
||||||
|
|
||||||
|
export const trackCommand: MdCommanderCommand = {
|
||||||
|
command: "track",
|
||||||
|
description: "添加一个新的追踪项目",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "tag",
|
||||||
|
description: "追踪项目的标签",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
class: {
|
||||||
|
option: "class",
|
||||||
|
description: "添加类(可多次使用)",
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
attr: {
|
||||||
|
option: "attr",
|
||||||
|
description: "添加属性,格式:name:type:value (type: progress/count/string)",
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (args, commands) => {
|
||||||
|
const tag = args.params.tag;
|
||||||
|
if (!tag) {
|
||||||
|
return { message: "错误:缺少 tag 参数", type: "error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析属性
|
||||||
|
const attributes: Record<string, any> = {};
|
||||||
|
const attrOption = args.options.attr;
|
||||||
|
|
||||||
|
if (attrOption) {
|
||||||
|
const attrs = Array.isArray(attrOption) ? attrOption : [attrOption];
|
||||||
|
for (const attrStr of attrs) {
|
||||||
|
const parts = attrStr.split(":");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const [name, typeStr, ...valueParts] = parts;
|
||||||
|
const type = typeStr as TrackerAttributeType;
|
||||||
|
const valueStr = valueParts.join(":");
|
||||||
|
|
||||||
|
let value: any;
|
||||||
|
if (type === "progress") {
|
||||||
|
const [x, y] = valueStr.split("/").map(Number);
|
||||||
|
value = { x: x || 0, y: y || 0 };
|
||||||
|
} else if (type === "count") {
|
||||||
|
value = parseInt(valueStr) || 0;
|
||||||
|
} else {
|
||||||
|
value = valueStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes[name] = { name, type, value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析类
|
||||||
|
const classes: string[] = [];
|
||||||
|
const classOption = args.options.class;
|
||||||
|
if (classOption) {
|
||||||
|
const cls = Array.isArray(classOption) ? classOption : [classOption];
|
||||||
|
classes.push(...cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过全局 commander 对象添加追踪项目
|
||||||
|
const event = new CustomEvent("md-commander-track", {
|
||||||
|
detail: { tag, classes, attributes },
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `已添加追踪项目:${tag}`,
|
||||||
|
type: "success",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const untrackCommand: MdCommanderCommand = {
|
||||||
|
command: "untrack",
|
||||||
|
description: "移除一个追踪项目",
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
description: "追踪项目的 ID",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handler: (args) => {
|
||||||
|
const id = args.params.id;
|
||||||
|
if (!id) {
|
||||||
|
return { message: "错误:缺少 id 参数", type: "error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new CustomEvent("md-commander-untrack", { detail: { id } });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
|
return { message: `已移除追踪项目:${id}`, type: "success" };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listTrackCommand: MdCommanderCommand = {
|
||||||
|
command: "list",
|
||||||
|
description: "列出所有追踪项目",
|
||||||
|
handler: (args, commands) => {
|
||||||
|
const event = new CustomEvent("md-commander-list-track");
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
|
return { message: "追踪列表已更新", type: "info" };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -2,3 +2,12 @@ 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,75 +1,39 @@
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal, onCleanup, onMount } 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";
|
||||||
|
|
||||||
// ==================== 默认命令 ====================
|
// ==================== 默认命令 ====================
|
||||||
|
|
||||||
export const defaultCommands: Record<string, MdCommanderCommand> = {
|
export const defaultCommands: MdCommanderCommandMap = {
|
||||||
help: {
|
help: setupHelpCommand({}),
|
||||||
command: "help",
|
clear: clearCommand,
|
||||||
description: "显示帮助信息或特定命令的帮助",
|
roll: rollCommand,
|
||||||
parameters: [
|
track: trackCommand,
|
||||||
{
|
untrack: untrackCommand,
|
||||||
name: "cmd",
|
list: listTrackCommand,
|
||||||
description: "要查询的命令名",
|
|
||||||
type: "enum",
|
|
||||||
values: [], // 运行时填充
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args) => {
|
|
||||||
const cmdName = args.params.cmd;
|
|
||||||
if (cmdName) {
|
|
||||||
return {
|
|
||||||
message: `命令:${cmdName}\n描述:${defaultCommands[cmdName]?.description || "无描述"}`,
|
|
||||||
type: "info",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 初始化默认命令
|
||||||
|
Object.keys(defaultCommands).forEach(key => {
|
||||||
|
if (key === "help") {
|
||||||
|
const help = setupHelpCommand(defaultCommands);
|
||||||
|
defaultCommands.help = help;
|
||||||
}
|
}
|
||||||
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",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
export function parseInput(input: string, commands?: Record<string, MdCommanderCommand>): {
|
export function parseInput(input: string, commands?: MdCommanderCommandMap): {
|
||||||
command?: string;
|
command?: string;
|
||||||
params: Record<string, string>;
|
params: Record<string, string>;
|
||||||
options: Record<string, string>;
|
options: Record<string, string>;
|
||||||
|
|
@ -156,7 +120,7 @@ export function parseInput(input: string, commands?: Record<string, MdCommanderC
|
||||||
|
|
||||||
export function getCompletions(
|
export function getCompletions(
|
||||||
input: string,
|
input: string,
|
||||||
commands: Record<string, MdCommanderCommand>,
|
commands: MdCommanderCommandMap,
|
||||||
): CompletionItem[] {
|
): CompletionItem[] {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
|
|
||||||
|
|
@ -264,15 +228,29 @@ export interface UseCommanderReturn {
|
||||||
handleCommand: () => void;
|
handleCommand: () => void;
|
||||||
updateCompletions: () => void;
|
updateCompletions: () => void;
|
||||||
acceptCompletion: () => void;
|
acceptCompletion: () => void;
|
||||||
commands: Record<string, MdCommanderCommand>;
|
commands: MdCommanderCommandMap;
|
||||||
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: (itemId: string) => void;
|
||||||
|
updateTrackerAttribute: (itemId: string, attrName: string, attr: TrackerAttribute) => void;
|
||||||
|
moveTrackerItem: (itemId: string, direction: 'up' | 'down') => void;
|
||||||
|
removeTrackerClass: (itemId: string, className: string) => void;
|
||||||
|
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommander(
|
export function useCommander(
|
||||||
customCommands?: Record<string, MdCommanderCommand>,
|
customCommands?: MdCommanderCommandMap,
|
||||||
|
element?: HTMLElement,
|
||||||
): UseCommanderReturn {
|
): UseCommanderReturn {
|
||||||
const [inputValue, setInputValue] = createSignal("");
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
||||||
|
|
@ -285,6 +263,11 @@ export function useCommander(
|
||||||
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 [trackerItems, setTrackerItemsState] = createSignal<TrackerItem[]>([]);
|
||||||
|
const [trackerHistory, setTrackerHistory] = createSignal<TrackerCommand[]>([]);
|
||||||
|
|
||||||
const commands = { ...defaultCommands, ...customCommands };
|
const commands = { ...defaultCommands, ...customCommands };
|
||||||
|
|
||||||
// 更新 help 命令的参数值
|
// 更新 help 命令的参数值
|
||||||
|
|
@ -294,6 +277,38 @@ export function useCommander(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
if (element) {
|
||||||
|
const handleTrack = (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent).detail as { tag: string; classes: string[]; attributes: Record<string, TrackerAttribute> };
|
||||||
|
addTrackerItem({
|
||||||
|
tag: detail.tag,
|
||||||
|
classes: detail.classes || [],
|
||||||
|
attributes: detail.attributes || {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUntrack = (e: Event) => {
|
||||||
|
const detail = (e as CustomEvent).detail as { id: string };
|
||||||
|
removeTrackerItem(detail.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListTrack = () => {
|
||||||
|
// 切换到 tracker 视图
|
||||||
|
setViewMode("tracker");
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener("md-commander-track", handleTrack as EventListener);
|
||||||
|
element.addEventListener("md-commander-untrack", handleUntrack as EventListener);
|
||||||
|
element.addEventListener("md-commander-list-track", handleListTrack as EventListener);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
element.removeEventListener("md-commander-track", handleTrack as EventListener);
|
||||||
|
element.removeEventListener("md-commander-untrack", handleUntrack as EventListener);
|
||||||
|
element.removeEventListener("md-commander-list-track", handleListTrack as EventListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleCommand = () => {
|
const handleCommand = () => {
|
||||||
const input = inputValue().trim();
|
const input = inputValue().trim();
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
@ -308,7 +323,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 });
|
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)}`,
|
||||||
|
|
@ -418,6 +433,88 @@ export function useCommander(
|
||||||
setInputValue(history[history.length - 1 - newIndex]);
|
setInputValue(history[history.length - 1 - newIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== Tracker 方法 ====================
|
||||||
|
|
||||||
|
const recordTrackerCommand = (cmd: Omit<TrackerCommand, 'timestamp'>) => {
|
||||||
|
setTrackerHistory((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ ...cmd, timestamp: new Date() },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => {
|
||||||
|
const newItem: TrackerItem = {
|
||||||
|
...item,
|
||||||
|
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
||||||
|
};
|
||||||
|
setTrackerItemsState((prev) => [...prev, newItem]);
|
||||||
|
recordTrackerCommand({
|
||||||
|
type: "add",
|
||||||
|
itemId: newItem.id,
|
||||||
|
data: newItem,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTrackerItem = (itemId: string) => {
|
||||||
|
const item = trackerItems().find((i) => i.id === itemId);
|
||||||
|
setTrackerItemsState((prev) => prev.filter((i) => i.id !== itemId));
|
||||||
|
recordTrackerCommand({
|
||||||
|
type: "remove",
|
||||||
|
itemId,
|
||||||
|
data: item,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTrackerAttribute = (itemId: string, attrName: string, attr: TrackerAttribute) => {
|
||||||
|
setTrackerItemsState((prev) =>
|
||||||
|
prev.map((i) =>
|
||||||
|
i.id === itemId
|
||||||
|
? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
|
||||||
|
: i
|
||||||
|
)
|
||||||
|
);
|
||||||
|
recordTrackerCommand({
|
||||||
|
type: "update",
|
||||||
|
itemId,
|
||||||
|
attributeUpdates: { [attrName]: attr },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveTrackerItem = (itemId: string, direction: 'up' | 'down') => {
|
||||||
|
const items = trackerItems();
|
||||||
|
const index = items.findIndex((i) => i.id === itemId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||||
|
if (newIndex < 0 || newIndex >= items.length) return;
|
||||||
|
|
||||||
|
const newItems = [...items];
|
||||||
|
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
|
||||||
|
setTrackerItemsState(newItems);
|
||||||
|
recordTrackerCommand({
|
||||||
|
type: "reorder",
|
||||||
|
data: newItems,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTrackerClass = (itemId: string, className: string) => {
|
||||||
|
setTrackerItemsState((prev) =>
|
||||||
|
prev.map((i) =>
|
||||||
|
i.id === itemId
|
||||||
|
? { ...i, classes: i.classes.filter((c) => c !== className) }
|
||||||
|
: i
|
||||||
|
)
|
||||||
|
);
|
||||||
|
recordTrackerCommand({
|
||||||
|
type: "update",
|
||||||
|
itemId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTrackerItems = (updater: (prev: TrackerItem[]) => TrackerItem[]) => {
|
||||||
|
setTrackerItemsState(updater);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputValue,
|
inputValue,
|
||||||
entries,
|
entries,
|
||||||
|
|
@ -438,5 +535,16 @@ export function useCommander(
|
||||||
setHistoryIndex,
|
setHistoryIndex,
|
||||||
commandHistory,
|
commandHistory,
|
||||||
navigateHistory,
|
navigateHistory,
|
||||||
|
viewMode,
|
||||||
|
setViewMode,
|
||||||
|
trackerItems,
|
||||||
|
setTrackerItems,
|
||||||
|
trackerHistory,
|
||||||
|
addTrackerItem,
|
||||||
|
removeTrackerItem,
|
||||||
|
updateTrackerAttribute,
|
||||||
|
moveTrackerItem,
|
||||||
|
removeTrackerClass,
|
||||||
|
recordTrackerCommand,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
import { onMount, onCleanup } from "solid-js";
|
import { onMount, onCleanup, createSignal, 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 type { MdCommanderProps } from "./types";
|
import { TrackerView } from "./TrackerView";
|
||||||
|
import { TabBar } from "./TabBar";
|
||||||
|
import { AttributeEditor } from "./AttributeEditor";
|
||||||
|
import type { MdCommanderProps, TrackerAttribute } from "./types";
|
||||||
|
|
||||||
customElement<MdCommanderProps>(
|
customElement<MdCommanderProps>(
|
||||||
"md-commander",
|
"md-commander",
|
||||||
|
|
@ -11,7 +14,12 @@ customElement<MdCommanderProps>(
|
||||||
(props, { element }) => {
|
(props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
const commander = useCommander(props.commands);
|
const commander = useCommander(props.commands, element as unknown as HTMLElement);
|
||||||
|
const [editingAttr, setEditingAttr] = createSignal<{
|
||||||
|
itemId: string;
|
||||||
|
attrName: string;
|
||||||
|
attr: TrackerAttribute;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (commander.showCompletions() && commander.completions().length > 0) {
|
if (commander.showCompletions() && commander.completions().length > 0) {
|
||||||
|
|
@ -35,7 +43,11 @@ customElement<MdCommanderProps>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
|
if (editingAttr()) {
|
||||||
|
setEditingAttr(null);
|
||||||
|
} else {
|
||||||
commander.setShowCompletions(false);
|
commander.setShowCompletions(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,11 +87,33 @@ 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
|
||||||
|
mode={commander.viewMode}
|
||||||
|
onModeChange={commander.setViewMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容区域:历史或追踪 */}
|
||||||
|
<Show
|
||||||
|
when={commander.viewMode() === "history"}
|
||||||
|
fallback={
|
||||||
|
<TrackerView
|
||||||
|
items={commander.trackerItems}
|
||||||
|
onEditAttribute={(itemId, attrName, attr) =>
|
||||||
|
setEditingAttr({ itemId, attrName, attr })
|
||||||
|
}
|
||||||
|
onRemoveClass={commander.removeTrackerClass}
|
||||||
|
onMoveUp={(itemId) => commander.moveTrackerItem(itemId, "up")}
|
||||||
|
onMoveDown={(itemId) => commander.moveTrackerItem(itemId, "down")}
|
||||||
|
onRemove={commander.removeTrackerItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<CommanderEntries
|
<CommanderEntries
|
||||||
entries={commander.entries}
|
entries={commander.entries}
|
||||||
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* 命令输入框 */}
|
{/* 命令输入框 */}
|
||||||
<div class="relative border-t border-gray-300">
|
<div class="relative border-t border-gray-300">
|
||||||
|
|
@ -104,6 +138,20 @@ customElement<MdCommanderProps>(
|
||||||
onAcceptCompletion={commander.acceptCompletion}
|
onAcceptCompletion={commander.acceptCompletion}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 属性编辑器弹窗 */}
|
||||||
|
<Show when={editingAttr()}>
|
||||||
|
<AttributeEditor
|
||||||
|
attribute={() => editingAttr()!.attr}
|
||||||
|
onUpdate={(attr) => {
|
||||||
|
const data = editingAttr();
|
||||||
|
if (data) {
|
||||||
|
commander.updateTrackerAttribute(data.itemId, data.attrName, attr);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClose={() => setEditingAttr(null)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ 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;
|
||||||
|
|
@ -15,7 +19,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;
|
||||||
|
|
@ -70,3 +74,30 @@ 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 {
|
||||||
|
id: string;
|
||||||
|
tag: string;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
23
todo.md
23
todo.md
|
|
@ -0,0 +1,23 @@
|
||||||
|
# todo
|
||||||
|
|
||||||
|
## md-commander(./components/md-commander)
|
||||||
|
|
||||||
|
- [ ] create a new file for each command
|
||||||
|
- [ ] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view.
|
||||||
|
- [ ] 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.
|
||||||
Loading…
Reference in New Issue