refactor: impl

This commit is contained in:
hypercross 2026-03-01 13:54:48 +08:00
parent 62780e3e56
commit bda201ae81
6 changed files with 187 additions and 62 deletions

View File

@ -1,27 +1,17 @@
import { type Component, For, Show, createEffect, on } from "solid-js"; import { type Component, For, Show } from "solid-js";
import type { CommanderEntry } from "./types"; import type { CommanderEntry } from "./types";
import { getResultClass } from "./hooks"; import { getResultClass } from "./hooks";
export interface CommanderEntriesProps { export interface CommanderEntriesProps {
entries: () => CommanderEntry[]; entries: () => CommanderEntry[];
onCommandClick?: (command: string) => void; onCommandClick?: (command: string) => void;
loading?: boolean;
error?: string;
} }
export const CommanderEntries: Component<CommanderEntriesProps> = (props) => { export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
let containerRef: HTMLDivElement | undefined; let containerRef: HTMLDivElement | undefined;
// 当 entries 变化时自动滚动到底部
createEffect(
on(
() => props.entries().length,
() => {
if (containerRef) {
containerRef.scrollTop = containerRef.scrollHeight;
}
},
),
);
const handleCommandClick = (command: string) => { const handleCommandClick = (command: string) => {
if (props.onCommandClick) { if (props.onCommandClick) {
props.onCommandClick(command); props.onCommandClick(command);
@ -34,35 +24,54 @@ export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2" class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2"
> >
<Show <Show
when={props.entries().length > 0} when={props.loading}
fallback={ fallback={
<div class="text-gray-400 text-center py-8"></div> <Show
when={props.error}
fallback={
<Show
when={props.entries().length > 0}
fallback={
<div class="text-gray-400 text-center py-8"></div>
}
>
<For each={props.entries()}>
{(entry) => (
<div class="border-l-2 border-gray-300 pl-3 py-1">
<div class="flex items-center justify-between text-xs text-gray-500 mb-1">
<span
class="font-mono cursor-pointer hover:text-blue-600 hover:underline"
onClick={() => handleCommandClick(entry.command)}
title="点击复制到输入框"
>
{entry.command}
</span>
<span>
{entry.timestamp.toLocaleTimeString()}
</span>
</div>
<div
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
innerHTML={entry.result.isHtml ? entry.result.message : undefined}
>
{!entry.result.isHtml && entry.result.message}
</div>
</div>
)}
</For>
</Show>
}
>
<div class="text-red-500 text-center py-8">{props.error}</div>
</Show>
} }
> >
<For each={props.entries()}> <div class="flex items-center justify-center h-full">
{(entry) => ( <div class="text-gray-500 flex items-center gap-2">
<div class="border-l-2 border-gray-300 pl-3 py-1"> <div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
<div class="flex items-center justify-between text-xs text-gray-500 mb-1"> <span>...</span>
<span </div>
class="font-mono cursor-pointer hover:text-blue-600 hover:underline" </div>
onClick={() => handleCommandClick(entry.command)}
title="点击复制到输入框"
>
{entry.command}
</span>
<span>
{entry.timestamp.toLocaleTimeString()}
</span>
</div>
<div
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
innerHTML={entry.result.isHtml ? entry.result.message : undefined}
>
{!entry.result.isHtml && entry.result.message}
</div>
</div>
)}
</For>
</Show> </Show>
</div> </div>
); );

View File

@ -4,20 +4,22 @@ import type {
CompletionItem, CompletionItem,
TrackerItem, TrackerItem,
TrackerAttribute, TrackerAttribute,
TrackerCommand,
TrackerViewMode, TrackerViewMode,
MdCommanderCommandMap, MdCommanderCommandMap,
TrackerCommand,
} from "../types"; } from "../types";
import { parseInput, getCompletions } from "./completions"; import { parseInput, getCompletions } from "./completions";
import { import {
addTrackerItem, addTrackerItem,
getTrackerHistory, getTrackerHistory,
getTrackerItems, getTrackerItems,
moveTrackerItem, removeTrackerClass, moveTrackerItem,
removeTrackerClass,
removeTrackerItem, removeTrackerItem,
updateTrackerAttribute, updateTrackerAttribute,
updateTrackerClasses updateTrackerClasses,
} from "../stores"; } from "../stores";
import { addEntry, getEntries, clearEntries } from "../stores/entriesStore";
// ==================== Commander Hook ==================== // ==================== Commander Hook ====================
@ -29,7 +31,6 @@ export interface UseCommanderReturn {
selectedCompletion: () => number; selectedCompletion: () => number;
isFocused: () => boolean; isFocused: () => boolean;
setInputValue: (v: string) => void; setInputValue: (v: string) => void;
setEntries: (updater: (prev: CommanderEntry[]) => CommanderEntry[]) => void;
setShowCompletions: (v: boolean) => void; setShowCompletions: (v: boolean) => void;
setSelectedCompletion: (v: number | ((prev: number) => number)) => void; setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
setIsFocused: (v: boolean) => void; setIsFocused: (v: boolean) => void;
@ -62,7 +63,6 @@ export interface UseCommanderReturn {
export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn { export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn {
const [inputValue, setInputValue] = createSignal(""); const [inputValue, setInputValue] = createSignal("");
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
const [showCompletions, setShowCompletions] = createSignal(false); const [showCompletions, setShowCompletions] = createSignal(false);
const [completions, setCompletions] = createSignal<CompletionItem[]>([]); const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
const [selectedCompletion, setSelectedCompletionState] = createSignal(0); const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
@ -72,6 +72,9 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history"); const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {}); const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {});
// 从 store 获取 entries
const entries = getEntries;
// ==================== 命令执行 ==================== // ==================== 命令执行 ====================
const handleCommand = () => { const handleCommand = () => {
@ -108,14 +111,15 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
timestamp: new Date(), timestamp: new Date(),
}; };
setEntries((prev) => [...prev, newEntry]); // 使用 store 添加记录
addEntry(newEntry);
setCommandHistory((prev) => [...prev, input]); setCommandHistory((prev) => [...prev, input]);
setHistoryIndex(-1); setHistoryIndex(-1);
setInputValue(""); setInputValue("");
setShowCompletions(false); setShowCompletions(false);
if (commandName === "clear") { if (commandName === "clear") {
setEntries([]); clearEntries();
} }
}; };
@ -246,7 +250,6 @@ export function useCommander(initialCommands?: MdCommanderCommandMap): UseComman
selectedCompletion, selectedCompletion,
isFocused, isFocused,
setInputValue, setInputValue,
setEntries,
setShowCompletions, setShowCompletions,
setSelectedCompletion, setSelectedCompletion,
setIsFocused, setIsFocused,

View File

@ -1,5 +1,5 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import { onMount, onCleanup, Show, createEffect, on } from "solid-js"; import { onMount, onCleanup, Show, createResource } from "solid-js";
import { useCommander } from "./hooks"; import { useCommander } from "./hooks";
import { CommanderInput } from "./CommanderInput"; import { CommanderInput } from "./CommanderInput";
import { CommanderEntries } from "./CommanderEntries"; import { CommanderEntries } from "./CommanderEntries";
@ -7,7 +7,7 @@ import { TrackerView } from "./TrackerView";
import { TabBar } from "./TabBar"; import { TabBar } from "./TabBar";
import type { MdCommanderProps } from "./types"; import type { MdCommanderProps } from "./types";
import { loadElementSrc } from "../utils/path"; import { loadElementSrc } from "../utils/path";
import { initializeCommands, loadCommandTemplatesFromCSV } from "./stores"; import { initializeCommands, loadCommandTemplatesFromCSV, getCommands, getCommandsLoading, getCommandsError } from "./stores";
customElement<MdCommanderProps>( customElement<MdCommanderProps>(
"md-commander", "md-commander",
@ -16,18 +16,28 @@ customElement<MdCommanderProps>(
noShadowDOM(); noShadowDOM();
const { articlePath, rawSrc } = loadElementSrc(element as any); const { articlePath, rawSrc } = loadElementSrc(element as any);
// 初始化命令并注册到 store // 初始化命令
const commands = initializeCommands(props.commands); const commands = initializeCommands(props.commands);
const commander = useCommander(commands); const commander = useCommander(commands);
// 加载 CSV 模板 // 使用 createResource 加载 CSV 模板
createEffect( async () => { const [templateData] = createResource(
if (!rawSrc || !rawSrc) return; () => (rawSrc ? { path: rawSrc, articlePath } : null),
await loadCommandTemplatesFromCSV(rawSrc, articlePath); async (paths) => {
// 更新 commander 中的命令(从 store 获取) await loadCommandTemplatesFromCSV(paths.path, paths.articlePath);
const {getCommands} = await import("./stores/commandsStore"); return getCommands();
commander.setCommands(getCommands()); }
}); );
// 当模板加载完成后更新 commander 中的命令
createResource(
() => templateData(),
(loadedCommands) => {
if (loadedCommands) {
commander.setCommands(loadedCommands);
}
}
);
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (commander.showCompletions() && commander.completions().length > 0) { if (commander.showCompletions() && commander.completions().length > 0) {
@ -114,6 +124,8 @@ customElement<MdCommanderProps>(
> >
<CommanderEntries <CommanderEntries
entries={commander.entries} entries={commander.entries}
loading={getCommandsLoading()}
error={getCommandsError()}
onCommandClick={(cmd) => commander.setInputValue(cmd)} onCommandClick={(cmd) => commander.setInputValue(cmd)}
/> />
</Show> </Show>

View File

@ -13,12 +13,17 @@ const defaultCommands: MdCommanderCommandMap = {
list: listTrackCommand, list: listTrackCommand,
}; };
const [commandsStore, setCommandsStore] = createStore<{ export interface CommandsStoreState {
commands: MdCommanderCommandMap; commands: MdCommanderCommandMap;
initialized: boolean; initialized: boolean;
}>({ loading: boolean;
error?: string;
}
const [commandsStore, setCommandsStore] = createStore<CommandsStoreState>({
commands: { ...defaultCommands }, commands: { ...defaultCommands },
initialized: false, initialized: false,
loading: false,
}); });
/** /**
@ -36,7 +41,7 @@ export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCo
*/ */
export function registerCommands(customCommands?: MdCommanderCommandMap): void { export function registerCommands(customCommands?: MdCommanderCommandMap): void {
const commands = initializeCommands(customCommands); const commands = initializeCommands(customCommands);
setCommandsStore({ commands, initialized: true }); setCommandsStore({ commands, initialized: true, loading: false });
} }
/** /**
@ -60,6 +65,41 @@ export function getCommand(name: string): MdCommanderCommand | undefined {
return commandsStore.commands[name]; return commandsStore.commands[name];
} }
/**
*
*/
export function getCommandsLoading(): boolean {
return commandsStore.loading;
}
/**
*
*/
export function getCommandsError(): string | undefined {
return commandsStore.error;
}
/**
*
*/
export function setCommandsLoading(loading: boolean): void {
setCommandsStore("loading", loading);
}
/**
*
*/
export function setCommandsError(error?: string): void {
setCommandsStore("error", error);
}
/**
*
*/
export function updateCommands(updater: (prev: MdCommanderCommandMap) => MdCommanderCommandMap): void {
setCommandsStore("commands", (prev) => updater(prev));
}
/** /**
* CSV * CSV
*/ */
@ -67,6 +107,9 @@ export async function loadCommandTemplatesFromCSV(
path: string, path: string,
articlePath: string articlePath: string
): Promise<void> { ): Promise<void> {
setCommandsLoading(true);
setCommandsError(undefined);
try { try {
const csv = await loadCSV<CommandTemplateRow>(resolvePath(articlePath, path)); const csv = await loadCSV<CommandTemplateRow>(resolvePath(articlePath, path));
@ -82,7 +125,7 @@ export async function loadCommandTemplatesFromCSV(
} }
// 为每个命令添加模板 // 为每个命令添加模板
setCommandsStore("commands", (prev) => { updateCommands((prev) => {
const updated = { ...prev }; const updated = { ...prev };
for (const [commandName, rows] of templatesByCommand.entries()) { for (const [commandName, rows] of templatesByCommand.entries()) {
const cmd = updated[commandName]; const cmd = updated[commandName];
@ -105,8 +148,14 @@ export async function loadCommandTemplatesFromCSV(
} }
return updated; return updated;
}); });
setCommandsStore("initialized", true);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
setCommandsError(`加载命令模板失败:${errorMessage}`);
console.warn(`Error loading command templates from ${path}:`, error); console.warn(`Error loading command templates from ${path}:`, error);
} finally {
setCommandsLoading(false);
} }
} }

View File

@ -0,0 +1,38 @@
import { createStore } from "solid-js/store";
import type { CommanderEntry } from "../types";
export interface EntriesStore {
entries: CommanderEntry[];
}
const [entriesStore, setEntriesStore] = createStore<EntriesStore>({
entries: [],
});
/**
*
*/
export function addEntry(entry: CommanderEntry): void {
setEntriesStore("entries", (prev) => [...prev, entry]);
}
/**
*
*/
export function clearEntries(): void {
setEntriesStore("entries", []);
}
/**
*
*/
export function getEntries(): CommanderEntry[] {
return entriesStore.entries;
}
/**
*
*/
export function setEntries(entries: CommanderEntry[]): void {
setEntriesStore("entries", entries);
}

View File

@ -20,4 +20,18 @@ export {
isCommandsInitialized, isCommandsInitialized,
getCommand, getCommand,
loadCommandTemplatesFromCSV, loadCommandTemplatesFromCSV,
getCommandsLoading,
getCommandsError,
setCommandsLoading,
setCommandsError,
updateCommands,
} from "./commandsStore"; } from "./commandsStore";
export type { CommandsStoreState } from "./commandsStore";
export {
addEntry,
clearEntries,
getEntries,
setEntries,
} from "./entriesStore";
export type { EntriesStore } from "./entriesStore";