refactor: impl
This commit is contained in:
parent
62780e3e56
commit
bda201ae81
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue