fix: issues

This commit is contained in:
hypercross 2026-03-01 10:01:53 +08:00
parent c23bff5a89
commit fdaf79e7ed
7 changed files with 178 additions and 68 deletions

View File

@ -1,13 +1,13 @@
import { type Component, For, Show } from "solid-js"; import { type Component, For, Show, Index } from "solid-js";
import type { TrackerItem, TrackerAttribute } from "./types"; import type { TrackerItem, TrackerAttribute } from "./types";
export interface TrackerViewProps { export interface TrackerViewProps {
items: () => TrackerItem[]; items: () => TrackerItem[];
onEditAttribute?: (itemId: string, attrName: string, attr: TrackerAttribute) => void; onEditAttribute?: (index: number, attrName: string, attr: TrackerAttribute) => void;
onRemoveClass?: (itemId: string, className: string) => void; onRemoveClass?: (index: number, className: string) => void;
onMoveUp?: (itemId: string) => void; onMoveUp?: (index: number) => void;
onMoveDown?: (itemId: string) => void; onMoveDown?: (index: number) => void;
onRemove?: (itemId: string) => void; onRemove?: (index: number) => void;
} }
export const TrackerView: Component<TrackerViewProps> = (props) => { export const TrackerView: Component<TrackerViewProps> = (props) => {
@ -32,7 +32,7 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
> >
<div class="space-y-4"> <div class="space-y-4">
<For each={props.items()}> <For each={props.items()}>
{(item) => ( {(item, index) => (
<div class="border border-gray-200 rounded-lg p-3 bg-gray-50"> <div class="border border-gray-200 rounded-lg p-3 bg-gray-50">
{/* 头部tag、id 和操作按钮 */} {/* 头部tag、id 和操作按钮 */}
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
@ -41,27 +41,26 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
<Show when={item.id}> <Show when={item.id}>
<span class="text-xs text-purple-600 font-mono">#{item.id}</span> <span class="text-xs text-purple-600 font-mono">#{item.id}</span>
</Show> </Show>
<span class="text-xs text-gray-500 font-mono"> {item.uuid.slice(-6)}</span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button <button
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded" class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"
title="上移" title="上移"
onClick={() => props.onMoveUp?.(item.id)} onClick={() => props.onMoveUp?.(index())}
> >
</button> </button>
<button <button
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded" class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded"
title="下移" title="下移"
onClick={() => props.onMoveDown?.(item.id)} onClick={() => props.onMoveDown?.(index())}
> >
</button> </button>
<button <button
class="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded" class="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded"
title="移除" title="移除"
onClick={() => props.onRemove?.(item.id)} onClick={() => props.onRemove?.(index())}
> >
× ×
</button> </button>
@ -71,20 +70,20 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
{/* Classes */} {/* Classes */}
<Show when={item.classes.length > 0}> <Show when={item.classes.length > 0}>
<div class="flex flex-wrap gap-1 mb-2"> <div class="flex flex-wrap gap-1 mb-2">
<For each={item.classes}> <Index each={item.classes}>
{(cls) => ( {(cls) => (
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded"> <span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded">
{cls} {cls()}
<button <button
class="hover:text-red-600" class="hover:text-red-600"
onClick={() => props.onRemoveClass?.(item.id, cls)} onClick={() => props.onRemoveClass?.(index(), cls())}
title="移除类" title="移除类"
> >
× ×
</button> </button>
</span> </span>
)} )}
</For> </Index>
</div> </div>
</Show> </Show>
@ -94,7 +93,7 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
{(attr: TrackerAttribute) => ( {(attr: TrackerAttribute) => (
<div <div
class="flex items-center justify-between px-2 py-1 bg-white rounded border border-gray-200 cursor-pointer hover:border-blue-400" 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)} onClick={() => props.onEditAttribute?.(index(), attr.name, attr)}
> >
<span class="text-xs text-gray-600">{attr.name}</span> <span class="text-xs text-gray-600">{attr.name}</span>
<span class="text-sm font-mono text-gray-800"> <span class="text-sm font-mono text-gray-800">

View File

@ -1,6 +1,6 @@
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types"; import type { MdCommanderCommand } from "../types";
import { parseEmmet } from "../utils/emmetParser"; import { parseEmmet } from "../utils";
import { addTrackerItem, removeTrackerItem, getTrackerItems } from "../stores"; import { addTrackerItem, removeTrackerItem, getTrackerItems, findTrackerItem } from "../stores";
export const trackCommand: MdCommanderCommand = { export const trackCommand: MdCommanderCommand = {
command: "track", command: "track",
@ -42,24 +42,32 @@ export const trackCommand: MdCommanderCommand = {
export const untrackCommand: MdCommanderCommand = { export const untrackCommand: MdCommanderCommand = {
command: "untrack", command: "untrack",
description: "移除一个追踪项目", description: "移除一个追踪项目 - 支持 Emmet 语法untrack #g2 或 untrack goblin.warrior",
parameters: [ parameters: [
{ {
name: "id", name: "emmet",
description: "追踪项目的 ID", description: "Emmet 格式:#id 或 tag.class",
type: "string", type: "string",
required: true, required: true,
}, },
], ],
handler: (args) => { handler: (args) => {
const id = args.params.id; const emmet = args.params.emmet;
if (!id) { if (!emmet) {
return { message: "错误:缺少 id 参数", type: "error" }; return { message: "错误:缺少 Emmet 参数", type: "error" };
} }
removeTrackerItem(id); const found = findTrackerItem(emmet);
if (!found) {
return { message: `未找到匹配的追踪项目:${emmet}`, type: "error" };
}
return { message: `已移除追踪项目:${id}`, type: "success" }; const success = removeTrackerItem(emmet);
if (!success) {
return { message: `移除失败:${emmet}`, type: "error" };
}
return { message: `已移除追踪项目:${found.tag}${found.id ? '#' + found.id : ''}`, type: "success" };
}, },
}; };

View File

@ -19,6 +19,7 @@ import {
removeTrackerClass as removeClassFromTracker, removeTrackerClass as removeClassFromTracker,
getTrackerItems, getTrackerItems,
getTrackerHistory, getTrackerHistory,
findTrackerIndex,
} from "../stores"; } from "../stores";
// ==================== 默认命令 ==================== // ==================== 默认命令 ====================
@ -250,10 +251,14 @@ export interface UseCommanderReturn {
setTrackerItems: (updater: (prev: TrackerItem[]) => TrackerItem[]) => void; setTrackerItems: (updater: (prev: TrackerItem[]) => TrackerItem[]) => void;
trackerHistory: () => TrackerCommand[]; trackerHistory: () => TrackerCommand[];
addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void; addTrackerItem: (item: Omit<TrackerItem, 'id'>) => void;
removeTrackerItem: (itemId: string) => void; removeTrackerItem: (emmet: string) => boolean;
updateTrackerAttribute: (itemId: string, attrName: string, attr: TrackerAttribute) => void; removeTrackerItemByIndex: (index: number) => void;
moveTrackerItem: (itemId: string, direction: 'up' | 'down') => void; updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
removeTrackerItemClass: (itemId: string, className: string) => void; updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean;
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void;
removeTrackerItemClass: (emmet: string, className: string) => boolean;
removeTrackerItemClassByIndex: (index: number, className: string) => void;
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void; recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
} }
@ -431,20 +436,56 @@ export function useCommander(
return addTracker(item); return addTracker(item);
}; };
const removeTrackerItem = (itemId: string) => { const removeTrackerItem = (emmet: string) => {
removeTracker(itemId); return removeTracker(emmet);
}; };
const updateTrackerAttribute = (itemId: string, attrName: string, attr: TrackerAttribute) => { const removeTrackerItemByIndex = (index: number) => {
updateTrackerAttr(itemId, attrName, attr); const items = trackerItems();
if (index >= 0 && index < items.length) {
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
removeTracker(emmet);
}
}; };
const moveTrackerItem = (itemId: string, direction: 'up' | 'down') => { const updateTrackerAttribute = (emmet: string, attrName: string, attr: TrackerAttribute) => {
moveTracker(itemId, direction); return updateTrackerAttr(emmet, attrName, attr);
}; };
const removeTrackerItemClass = (itemId: string, className: string) => { const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
removeClassFromTracker(itemId, className); const items = trackerItems();
if (index >= 0 && index < items.length) {
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
updateTrackerAttr(emmet, attrName, attr);
}
};
const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
return moveTracker(emmet, direction);
};
const moveTrackerItemByIndex = (index: number, direction: 'up' | 'down') => {
const items = trackerItems();
if (index >= 0 && index < items.length) {
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
moveTracker(emmet, direction);
}
};
const removeTrackerItemClass = (emmet: string, className: string) => {
return removeClassFromTracker(emmet, className);
};
const removeTrackerItemClassByIndex = (index: number, className: string) => {
const items = trackerItems();
if (index >= 0 && index < items.length) {
const item = items[index];
const emmet = `${item.tag}${item.id ? '#' + item.id : ''}${item.classes.length > 0 ? '.' + item.classes.join('.') : ''}`;
removeClassFromTracker(emmet, className);
}
}; };
const trackerItems = () => getTrackerItems(); const trackerItems = () => getTrackerItems();
@ -491,9 +532,13 @@ export function useCommander(
trackerHistory, trackerHistory,
addTrackerItem, addTrackerItem,
removeTrackerItem, removeTrackerItem,
removeTrackerItemByIndex,
updateTrackerAttribute, updateTrackerAttribute,
updateTrackerAttributeByIndex,
moveTrackerItem, moveTrackerItem,
moveTrackerItemByIndex,
removeTrackerItemClass, removeTrackerItemClass,
removeTrackerItemClassByIndex,
recordTrackerCommand, recordTrackerCommand,
}; };
} }

View File

@ -16,7 +16,7 @@ customElement<MdCommanderProps>(
const commander = useCommander(props.commands); const commander = useCommander(props.commands);
const [editingAttr, setEditingAttr] = createSignal<{ const [editingAttr, setEditingAttr] = createSignal<{
itemId: string; index: number;
attrName: string; attrName: string;
attr: TrackerAttribute; attr: TrackerAttribute;
} | null>(null); } | null>(null);
@ -99,13 +99,13 @@ customElement<MdCommanderProps>(
fallback={ fallback={
<TrackerView <TrackerView
items={commander.trackerItems} items={commander.trackerItems}
onEditAttribute={(itemId, attrName, attr) => onEditAttribute={(index, attrName, attr) =>
setEditingAttr({ itemId, attrName, attr }) setEditingAttr({ index, attrName, attr })
} }
onRemoveClass={commander.removeTrackerItemClass} onRemoveClass={commander.removeTrackerItemClassByIndex}
onMoveUp={(itemId) => commander.moveTrackerItem(itemId, "up")} onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(itemId) => commander.moveTrackerItem(itemId, "down")} onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
onRemove={commander.removeTrackerItem} onRemove={(index) => commander.removeTrackerItemByIndex(index)}
/> />
} }
> >
@ -146,7 +146,7 @@ customElement<MdCommanderProps>(
onUpdate={(attr) => { onUpdate={(attr) => {
const data = editingAttr(); const data = editingAttr();
if (data) { if (data) {
commander.updateTrackerAttribute(data.itemId, data.attrName, attr); commander.updateTrackerAttributeByIndex(data.index, data.attrName, attr);
} }
}} }}
onClose={() => setEditingAttr(null)} onClose={() => setEditingAttr(null)}

View File

@ -7,5 +7,7 @@ export {
getTrackerItems, getTrackerItems,
getTrackerHistory, getTrackerHistory,
clearTrackerHistory, clearTrackerHistory,
findTrackerIndex,
findTrackerItem,
} from "./trackerStore"; } from "./trackerStore";
export type { TrackerStore } from "./trackerStore"; export type { TrackerStore } from "./trackerStore";

View File

@ -11,10 +11,51 @@ const [tracker, setTracker] = createStore<TrackerStore>({
history: [], history: [],
}); });
/**
* Emmet
*
* - #id - id
* - tag - tag
* - tag.class - tag class
* - .class - class
*/
export function findTrackerIndex(emmet: string): number {
const trimmed = emmet.trim();
// 解析 #id
if (trimmed.startsWith('#')) {
const id = trimmed.slice(1).split('.')[0];
return tracker.items.findIndex(item => item.id === id);
}
// 解析 tag 和 classes
const match = trimmed.match(/^([^.#]+)?(?:#([^.#]+))?(?:\.(.+))?$/);
if (!match) return -1;
const tag = match[1];
const classes = match[3] ? match[3].split('.') : [];
return tracker.items.findIndex(item => {
// 匹配 tag
if (tag && item.tag !== tag) return false;
// 匹配所有 classes
if (classes.length > 0) {
for (const cls of classes) {
if (!item.classes.includes(cls)) return false;
}
}
return true;
});
}
export function findTrackerItem(emmet: string): TrackerItem | undefined {
const index = findTrackerIndex(emmet);
return index >= 0 ? tracker.items[index] : undefined;
}
export function addTrackerItem(item: Omit<TrackerItem, "uuid">): TrackerItem { export function addTrackerItem(item: Omit<TrackerItem, "uuid">): TrackerItem {
const newItem: TrackerItem = { const newItem: TrackerItem = {
...item, ...item,
uuid: Date.now().toString() + Math.random().toString(36).slice(2),
}; };
setTracker("items", (prev) => [...prev, newItem]); setTracker("items", (prev) => [...prev, newItem]);
@ -31,29 +72,38 @@ export function addTrackerItem(item: Omit<TrackerItem, "uuid">): TrackerItem {
return newItem; return newItem;
} }
export function removeTrackerItem(itemId: string) { export function removeTrackerItem(emmet: string): boolean {
const item = tracker.items.find((i) => i.uuid === itemId); const index = findTrackerIndex(emmet);
if (index === -1) return false;
setTracker("items", (prev) => prev.filter((i) => i.uuid !== itemId)); const item = tracker.items[index];
setTracker("items", (prev) => prev.filter((_, i) => i !== index));
setTracker("history", (prev) => [ setTracker("history", (prev) => [
...prev, ...prev,
{ {
type: "remove", type: "remove",
itemId, itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
data: item, data: item,
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
return true;
} }
export function updateTrackerAttribute( export function updateTrackerAttribute(
itemId: string, emmet: string,
attrName: string, attrName: string,
attr: TrackerAttribute attr: TrackerAttribute
) { ): boolean {
const index = findTrackerIndex(emmet);
if (index === -1) return false;
const item = tracker.items[index];
setTracker("items", (prev) => setTracker("items", (prev) =>
prev.map((i) => prev.map((i, idx) =>
i.uuid === itemId idx === index
? { ...i, attributes: { ...i.attributes, [attrName]: attr } } ? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
: i : i
) )
@ -62,23 +112,24 @@ export function updateTrackerAttribute(
...prev, ...prev,
{ {
type: "update", type: "update",
itemId, itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
attributeUpdates: { [attrName]: attr }, attributeUpdates: { [attrName]: attr },
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
return true;
} }
export function moveTrackerItem(itemId: string, direction: "up" | "down") { export function moveTrackerItem(emmet: string, direction: "up" | "down"): boolean {
const index = tracker.items.findIndex((i) => i.uuid === itemId); const index = findTrackerIndex(emmet);
if (index === -1) return; if (index === -1) return false;
const newIndex = direction === "up" ? index - 1 : index + 1; const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= tracker.items.length) return; if (newIndex < 0 || newIndex >= tracker.items.length) return false;
const newItems = [...tracker.items]; const newItems = [...tracker.items];
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]]; [newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
setTracker("items", newItems); setTracker("items", newItems);
setTracker("history", (prev) => [ setTracker("history", (prev) => [
...prev, ...prev,
@ -88,12 +139,17 @@ export function moveTrackerItem(itemId: string, direction: "up" | "down") {
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
return true;
} }
export function removeTrackerClass(itemId: string, className: string) { export function removeTrackerClass(emmet: string, className: string): boolean {
const index = findTrackerIndex(emmet);
if (index === -1) return false;
const item = tracker.items[index];
setTracker("items", (prev) => setTracker("items", (prev) =>
prev.map((i) => prev.map((i, idx) =>
i.uuid === itemId idx === index
? { ...i, classes: i.classes.filter((c) => c !== className) } ? { ...i, classes: i.classes.filter((c) => c !== className) }
: i : i
) )
@ -102,10 +158,11 @@ export function removeTrackerClass(itemId: string, className: string) {
...prev, ...prev,
{ {
type: "update", type: "update",
itemId, itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
timestamp: new Date(), timestamp: new Date(),
}, },
]); ]);
return true;
} }
export function getTrackerItems(): TrackerItem[] { export function getTrackerItems(): TrackerItem[] {

View File

@ -86,9 +86,8 @@ export interface TrackerAttribute {
} }
export interface TrackerItem { export interface TrackerItem {
uuid: string; // 内部唯一标识符
tag: string; tag: string;
id?: string; // Emmet ID (#id),可选的用户定义 ID id?: string; // Emmet ID (#id)
classes: string[]; classes: string[];
attributes: Record<string, TrackerAttribute>; attributes: Record<string, TrackerAttribute>;
} }