refactor: impl

This commit is contained in:
hypercross 2026-03-01 12:41:03 +08:00
parent c2c3956a82
commit 5d026dfd80
7 changed files with 202 additions and 149 deletions

View File

@ -39,4 +39,5 @@ export { TabBar } from './md-commander/TabBar';
export { TrackerView } from './md-commander/TrackerView'; export { TrackerView } from './md-commander/TrackerView';
export { CommanderEntries } from './md-commander/CommanderEntries'; export { CommanderEntries } from './md-commander/CommanderEntries';
export { CommanderInput } from './md-commander/CommanderInput'; export { CommanderInput } from './md-commander/CommanderInput';
export { useCommander, defaultCommands } from './md-commander/hooks'; export { useCommander } from './md-commander/hooks';
export { initializeCommands, getCommands } from './md-commander/stores/commandsStore';

View File

@ -1,4 +1,4 @@
export { useCommander, initializeCommands, defaultCommands } from "./useCommander"; export { useCommander } from "./useCommander";
export type { UseCommanderReturn } from "./useCommander"; export type { UseCommanderReturn } from "./useCommander";
export { rollSimple, rollFormula } from "./useDiceRoller"; export { rollSimple, rollFormula } from "./useDiceRoller";
export type { DiceRollerResult } from "./useDiceRoller"; export type { DiceRollerResult } from "./useDiceRoller";

View File

@ -1,45 +1,14 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import type { import type {
MdCommanderCommand,
MdCommanderCommandMap,
CommanderEntry, CommanderEntry,
CompletionItem, CompletionItem,
TrackerItem, TrackerItem,
TrackerAttribute, TrackerAttribute,
TrackerCommand, TrackerCommand,
TrackerViewMode, TrackerViewMode,
MdCommanderCommandMap,
} from "../types"; } from "../types";
import { parseInput, getCompletions } from "./completions"; import { parseInput, getCompletions } from "./completions";
import { 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 = {
help: { command: "help" }, // 占位,稍后初始化
clear: clearCommand,
roll: rollCommand,
track: trackCommand,
untrack: untrackCommand,
list: listTrackCommand,
};
// 初始化默认命令(包括 help
export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap {
const commands = { ...defaultCommands, ...customCommands };
commands.help = setupHelpCommand(commands);
return commands;
}
// ==================== Commander Hook ==================== // ==================== Commander Hook ====================
@ -58,30 +27,31 @@ export interface UseCommanderReturn {
handleCommand: () => void; handleCommand: () => void;
updateCompletions: () => void; updateCompletions: () => void;
acceptCompletion: () => void; acceptCompletion: () => void;
commands: MdCommanderCommandMap; commands: () => MdCommanderCommandMap;
setCommands: (cmds: MdCommanderCommandMap) => void;
historyIndex: () => number; historyIndex: () => number;
setHistoryIndex: (v: number) => void; setHistoryIndex: (v: number) => void;
commandHistory: () => string[]; commandHistory: () => string[];
navigateHistory: (direction: 'up' | 'down') => void; navigateHistory: (direction: "up" | "down") => void;
// Tracker 相关 - 直接暴露 store API // Tracker 相关
viewMode: () => TrackerViewMode; viewMode: () => TrackerViewMode;
setViewMode: (mode: TrackerViewMode) => void; setViewMode: (mode: TrackerViewMode) => void;
trackerItems: () => TrackerItem[]; trackerItems: () => TrackerItem[];
trackerHistory: () => TrackerCommand[]; trackerHistory: () => TrackerCommand[];
addTrackerItem: typeof addTracker; addTrackerItem: (item: Omit<TrackerItem, "id">) => TrackerItem;
removeTrackerItem: typeof removeTracker; removeTrackerItem: (emmet: string) => boolean;
removeTrackerItemByIndex: (index: number) => void; removeTrackerItemByIndex: (index: number) => void;
updateTrackerAttribute: typeof updateTrackerAttr; updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void; updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
updateTrackerClassesByIndex: (index: number, classes: string[]) => void; updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
moveTrackerItem: typeof moveTracker; moveTrackerItem: (emmet: string, direction: "up" | "down") => boolean;
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void; moveTrackerItemByIndex: (index: number, direction: "up" | "down") => void;
removeTrackerItemClass: typeof removeClassFromTracker; removeTrackerItemClass: (emmet: string, className: string) => boolean;
removeTrackerItemClassByIndex: (index: number, className: string) => void; removeTrackerItemClassByIndex: (index: number, className: string) => void;
} }
export function useCommander(customCommands?: MdCommanderCommandMap): UseCommanderReturn { export function useCommander(initialCommands?: MdCommanderCommandMap): 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);
@ -91,8 +61,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
const [commandHistory, setCommandHistory] = createSignal<string[]>([]); const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1); const [historyIndex, setHistoryIndex] = createSignal(-1);
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history"); const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {});
const commands = initializeCommands(customCommands);
// ==================== 命令执行 ==================== // ==================== 命令执行 ====================
@ -100,9 +69,10 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
const input = inputValue().trim(); const input = inputValue().trim();
if (!input) return; if (!input) return;
const parsed = parseInput(input, commands); const cmds = commands();
const parsed = parseInput(input, cmds);
const commandName = parsed.command; const commandName = parsed.command;
const cmd = commands[commandName!]; const cmd = cmds[commandName!];
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean }; let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
@ -110,7 +80,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
result = { message: `未知命令:${commandName}`, type: "error" }; result = { message: `未知命令:${commandName}`, type: "error" };
} else if (cmd.handler) { } else if (cmd.handler) {
try { try {
result = cmd.handler({ params: parsed.params, options: parsed.options }, commands); result = cmd.handler({ params: parsed.params, options: parsed.options }, cmds);
} catch (e) { } catch (e) {
result = { result = {
message: `执行错误:${e instanceof Error ? e.message : String(e)}`, message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
@ -143,7 +113,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
// ==================== 自动补全 ==================== // ==================== 自动补全 ====================
const updateCompletions = () => { const updateCompletions = () => {
const comps = getCompletions(inputValue(), commands); const comps = getCompletions(inputValue(), commands());
setCompletions(comps); setCompletions(comps);
setShowCompletions(comps.length > 0 && isFocused()); setShowCompletions(comps.length > 0 && isFocused());
setSelectedCompletionState(0); setSelectedCompletionState(0);
@ -151,8 +121,8 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
const setSelectedCompletion = (v: number | ((prev: number) => number)) => { const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
const maxIdx = completions().length - 1; const maxIdx = completions().length - 1;
if (typeof v === 'function') { if (typeof v === "function") {
setSelectedCompletionState(prev => { setSelectedCompletionState((prev) => {
const next = v(prev); const next = v(prev);
return Math.max(0, Math.min(next, maxIdx)); return Math.max(0, Math.min(next, maxIdx));
}); });
@ -168,7 +138,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
if (!comp) return; if (!comp) return;
const input = inputValue(); const input = inputValue();
const parsed = parseInput(input, commands); const parsed = parseInput(input, commands());
let newValue: string; let newValue: string;
if (comp.type === "command") { if (comp.type === "command") {
@ -179,7 +149,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
.join(" "); .join(" ");
newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
} else if (comp.type === "value") { } else if (comp.type === "value") {
const cmd = parsed.command ? commands[parsed.command] : null; const cmd = parsed.command ? commands()[parsed.command] : null;
const paramDefs = cmd?.parameters || []; const paramDefs = cmd?.parameters || [];
const usedParams = Object.keys(parsed.params); const usedParams = Object.keys(parsed.params);
const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" "); const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" ");
@ -204,12 +174,12 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
// ==================== 命令历史 ==================== // ==================== 命令历史 ====================
const navigateHistory = (direction: 'up' | 'down') => { const navigateHistory = (direction: "up" | "down") => {
const history = commandHistory(); const history = commandHistory();
if (history.length === 0) return; if (history.length === 0) return;
let newIndex = historyIndex(); let newIndex = historyIndex();
if (direction === 'up') { if (direction === "up") {
if (newIndex < history.length - 1) newIndex++; if (newIndex < history.length - 1) newIndex++;
} else { } else {
if (newIndex > 0) { if (newIndex > 0) {
@ -226,37 +196,47 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
}; };
// ==================== Tracker 操作 ==================== // ==================== Tracker 操作 ====================
const {
getTrackerItems,
getTrackerHistory,
addTrackerItem,
removeTrackerItem,
updateTrackerAttribute,
updateTrackerClasses,
moveTrackerItem,
removeTrackerClass,
} = require("../stores") as typeof import("../stores");
const getEmmetFromIndex = (index: number): string | null => { const getEmmetFromIndex = (index: number): string | null => {
const items = getTrackerItems(); const items = getTrackerItems();
if (index < 0 || index >= items.length) return null; if (index < 0 || index >= items.length) return null;
const item = items[index]; const item = items[index];
return `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`; return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`;
}; };
const removeTrackerItemByIndex = (index: number) => { const removeTrackerItemByIndex = (index: number) => {
const emmet = getEmmetFromIndex(index); const emmet = getEmmetFromIndex(index);
if (emmet) removeTracker(emmet); if (emmet) removeTrackerItem(emmet);
}; };
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => { const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
const emmet = getEmmetFromIndex(index); const emmet = getEmmetFromIndex(index);
if (emmet) updateTrackerAttr(emmet, attrName, attr); if (emmet) updateTrackerAttribute(emmet, attrName, attr);
}; };
const updateTrackerClassesByIndex = (index: number, classes: string[]) => { const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
const emmet = getEmmetFromIndex(index); const emmet = getEmmetFromIndex(index);
if (emmet) updateTrackerClassesStore(emmet, classes); if (emmet) updateTrackerClasses(emmet, classes);
}; };
const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => { const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => {
const emmet = getEmmetFromIndex(index); const emmet = getEmmetFromIndex(index);
if (emmet) moveTracker(emmet, direction); if (emmet) moveTrackerItem(emmet, direction);
}; };
const removeTrackerItemClassByIndex = (index: number, className: string) => { const removeTrackerItemClassByIndex = (index: number, className: string) => {
const emmet = getEmmetFromIndex(index); const emmet = getEmmetFromIndex(index);
if (emmet) removeClassFromTracker(emmet, className); if (emmet) removeTrackerClass(emmet, className);
}; };
return { return {
@ -275,6 +255,7 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
updateCompletions, updateCompletions,
acceptCompletion, acceptCompletion,
commands, commands,
setCommands,
historyIndex, historyIndex,
setHistoryIndex, setHistoryIndex,
commandHistory, commandHistory,
@ -283,15 +264,15 @@ export function useCommander(customCommands?: MdCommanderCommandMap): UseCommand
setViewMode, setViewMode,
trackerItems: getTrackerItems, trackerItems: getTrackerItems,
trackerHistory: getTrackerHistory, trackerHistory: getTrackerHistory,
addTrackerItem: addTracker, addTrackerItem,
removeTrackerItem: removeTracker, removeTrackerItem,
removeTrackerItemByIndex, removeTrackerItemByIndex,
updateTrackerAttribute: updateTrackerAttr, updateTrackerAttribute,
updateTrackerAttributeByIndex, updateTrackerAttributeByIndex,
updateTrackerClassesByIndex, updateTrackerClassesByIndex,
moveTrackerItem: moveTracker, moveTrackerItem,
moveTrackerItemByIndex, moveTrackerItemByIndex,
removeTrackerItemClass: removeClassFromTracker, removeTrackerItemClass,
removeTrackerItemClassByIndex, removeTrackerItemClassByIndex,
}; };
} }

View File

@ -1,13 +1,13 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import { onMount, onCleanup, Show, createEffect } from "solid-js"; import { onMount, onCleanup, Show, createEffect, on } from "solid-js";
import { useCommander, initializeCommands } from "./hooks"; import { useCommander } from "./hooks";
import { CommanderInput } from "./CommanderInput"; import { CommanderInput } from "./CommanderInput";
import { CommanderEntries } from "./CommanderEntries"; import { CommanderEntries } from "./CommanderEntries";
import { TrackerView } from "./TrackerView"; import { TrackerView } from "./TrackerView";
import { TabBar } from "./TabBar"; import { TabBar } from "./TabBar";
import type { MdCommanderProps } from "./types"; import type { MdCommanderProps } from "./types";
import { loadElementSrc, resolvePath } from "../utils/path"; import { loadElementSrc, resolvePath } from "../utils/path";
import { loadCommandTemplates } from "./utils/commandTemplates"; import { initializeCommands, loadCommandTemplatesFromCSV } from "./stores/commandsStore";
customElement<MdCommanderProps>( customElement<MdCommanderProps>(
"md-commander", "md-commander",
@ -15,13 +15,21 @@ customElement<MdCommanderProps>(
(props, { element }) => { (props, { element }) => {
noShadowDOM(); noShadowDOM();
const { articlePath, rawSrc } = loadElementSrc(element as any); const { articlePath, rawSrc } = loadElementSrc(element as any);
const commander = useCommander(props.commands);
createEffect(async () => { // 初始化命令并注册到 store
if (!rawSrc) return; const commands = initializeCommands(props.commands);
const commands = initializeCommands(props.commands); const commander = useCommander(commands);
await loadCommandTemplates(commands, resolvePath(articlePath, rawSrc));
}); // 加载 CSV 模板
createEffect(
on(() => props.commandTemplates, async (csvPaths) => {
if (!csvPaths || !rawSrc) return;
await loadCommandTemplatesFromCSV(csvPaths, resolvePath, articlePath);
// 更新 commander 中的命令(从 store 获取)
const { getCommands } = await import("./stores/commandsStore");
commander.setCommands(getCommands());
}),
);
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (commander.showCompletions() && commander.completions().length > 0) { if (commander.showCompletions() && commander.completions().length > 0) {

View File

@ -0,0 +1,123 @@
import { createStore } from "solid-js/store";
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
const defaultCommands: MdCommanderCommandMap = {
help: setupHelpCommand({}),
clear: clearCommand,
roll: rollCommand,
track: trackCommand,
untrack: untrackCommand,
list: listTrackCommand,
};
const [commandsStore, setCommandsStore] = createStore<{
commands: MdCommanderCommandMap;
initialized: boolean;
}>({
commands: { ...defaultCommands },
initialized: false,
});
/**
* help
*/
export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap {
const commands = { ...defaultCommands, ...customCommands };
// 更新 help 命令的命令列表
commands.help = setupHelpCommand(commands);
return commands;
}
/**
* store
*/
export function registerCommands(customCommands?: MdCommanderCommandMap): void {
const commands = initializeCommands(customCommands);
setCommandsStore({ commands, initialized: true });
}
/**
* store
*/
export function getCommands(): MdCommanderCommandMap {
return commandsStore.commands;
}
/**
*
*/
export function isCommandsInitialized(): boolean {
return commandsStore.initialized;
}
/**
*
*/
export function getCommand(name: string): MdCommanderCommand | undefined {
return commandsStore.commands[name];
}
/**
* CSV
*/
export async function loadCommandTemplatesFromCSV(
csvPaths: string | string[],
resolvePath: (base: string, path: string) => string,
articlePath: string
): Promise<void> {
const paths = Array.isArray(csvPaths) ? csvPaths : [csvPaths];
for (const path of paths) {
try {
const { loadCSV } = await import("../../utils/csv-loader");
const csv = await loadCSV<CommandTemplateRow>(resolvePath(articlePath, path));
// 按命令分组模板
const templatesByCommand = new Map<string, CommandTemplateRow[]>();
for (const row of csv) {
if (!row.command || !row.label || !row.insertedText) continue;
if (!templatesByCommand.has(row.command)) {
templatesByCommand.set(row.command, []);
}
templatesByCommand.get(row.command)!.push(row);
}
// 为每个命令添加模板
setCommandsStore("commands", (prev) => {
const updated = { ...prev };
for (const [commandName, rows] of templatesByCommand.entries()) {
const cmd = updated[commandName];
if (!cmd || !cmd.parameters) continue;
const templates = rows.map((row) => ({
label: row.label,
description: row.description || "",
insertText: row.insertedText,
}));
// 为每个参数添加模板
updated[commandName] = {
...cmd,
parameters: cmd.parameters.map((param) => ({
...param,
templates: param.templates ? [...param.templates, ...templates] : templates,
})),
};
}
return updated;
});
} catch (error) {
console.warn(`Error loading command templates from ${path}:`, error);
}
}
}
interface CommandTemplateRow {
command: string;
parameter: string;
label: string;
description: string;
insertedText: string;
}

View File

@ -12,3 +12,12 @@ export {
findTrackerItem, findTrackerItem,
} from "./trackerStore"; } from "./trackerStore";
export type { TrackerStore } from "./trackerStore"; export type { TrackerStore } from "./trackerStore";
export {
initializeCommands,
registerCommands,
getCommands,
isCommandsInitialized,
getCommand,
loadCommandTemplatesFromCSV,
} from "./commandsStore";

View File

@ -1,69 +0,0 @@
import type { MdCommanderCommandMap, MdCommanderTemplate } from "../types";
import { loadCSV } from "../../utils/csv-loader";
export interface CommandTemplateRow {
command: string;
parameter: string;
label: string;
description: string;
insertedText: string;
}
/**
* CSV
*/
function buildTemplatesFromRows(rows: CommandTemplateRow[]): MdCommanderTemplate[] {
return rows.map(row => ({
label: row.label,
description: row.description || "",
insertText: row.insertedText,
}));
}
/**
* CSV
*/
export async function loadCommandTemplates(
commands: MdCommanderCommandMap,
csvPaths: string | string[]
): Promise<MdCommanderCommandMap> {
const paths = Array.isArray(csvPaths) ? csvPaths : [csvPaths];
for (const path of paths) {
try {
const csv = await loadCSV<CommandTemplateRow>(path);
// 按命令分组模板
const templatesByCommand = new Map<string, CommandTemplateRow[]>();
for (const row of csv) {
if (!row.command || !row.label || !row.insertedText) continue;
if (!templatesByCommand.has(row.command)) {
templatesByCommand.set(row.command, []);
}
templatesByCommand.get(row.command)!.push(row);
}
// 为每个命令添加模板
for (const [commandName, rows] of templatesByCommand.entries()) {
const cmd = commands[commandName];
if (!cmd || !cmd.parameters) continue;
const templates = buildTemplatesFromRows(rows);
// 为每个参数添加模板
for (const param of cmd.parameters) {
if (!param.templates) {
param.templates = [];
}
// 添加新模板
param.templates.push(...templates);
}
}
} catch (error) {
console.warn(`Error loading command templates from ${path}:`, error);
}
}
return commands;
}