feat: edit entry classes

This commit is contained in:
hypercross 2026-03-01 10:33:55 +08:00
parent 80a6b4526c
commit abaee79198
6 changed files with 112 additions and 1 deletions

View File

@ -4,18 +4,21 @@ import type { TrackerAttribute } from "./types";
export interface AttributeTooltipProps { export interface AttributeTooltipProps {
position: { x: number; y: number }; position: { x: number; y: number };
attributes: Record<string, TrackerAttribute>; attributes: Record<string, TrackerAttribute>;
classes: string[];
onUpdate: (attrName: string, attr: TrackerAttribute) => void; onUpdate: (attrName: string, attr: TrackerAttribute) => void;
onClassesChange: (classes: string[]) => void;
onClose: () => void; onClose: () => void;
} }
export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => { export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
const [position, setPosition] = createSignal(props.position); const [position, setPosition] = createSignal(props.position);
const [newClass, setNewClass] = createSignal("");
// 点击外部关闭 // 点击外部关闭
createEffect(() => { createEffect(() => {
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (!target.closest(".attribute-tooltip")) { if (target.closest('body') && !target.closest(".attribute-tooltip")) {
props.onClose(); props.onClose();
} }
}; };
@ -29,6 +32,22 @@ export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
props.onUpdate(attrName, { ...attr, value }); props.onUpdate(attrName, { ...attr, value });
}; };
// 添加 class
const handleAddClass = () => {
const className = newClass().trim();
if (className && !props.classes.includes(className)) {
props.onClassesChange([...props.classes, className]);
setNewClass("");
}
};
// 移除 class
const handleRemoveClass = (e: MouseEvent, className: string) => {
e.preventDefault();
e.stopPropagation();
props.onClassesChange(props.classes.filter((c) => c !== className));
};
// 渲染不同类型的属性编辑器 // 渲染不同类型的属性编辑器
const renderEditor = (attrName: string, attr: TrackerAttribute) => { const renderEditor = (attrName: string, attr: TrackerAttribute) => {
if (attr.type === "progress") { if (attr.type === "progress") {
@ -125,6 +144,49 @@ export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
</button> </button>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
{/* Class 列表编辑器 */}
<div class="flex flex-col gap-1">
<label class="text-xs text-gray-500 font-medium">Classes</label>
<div class="flex flex-wrap gap-1 mb-1">
<For each={props.classes}>
{(className) => (
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">
.{className}
<button
class="text-blue-400 hover:text-blue-600 font-bold leading-none"
onClick={(e) => handleRemoveClass(e, className)}
>
×
</button>
</span>
)}
</For>
</div>
<div class="flex items-center gap-1">
<input
type="text"
value={newClass()}
onInput={(e) => setNewClass((e.target as HTMLInputElement).value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
handleAddClass();
}
}}
placeholder="添加 class..."
class="flex-1 px-2 py-1 border border-gray-300 rounded text-sm"
/>
<button
class="px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded text-sm font-medium transition-colors"
onClick={handleAddClass}
>
</button>
</div>
</div>
{/* 属性编辑器 */}
<For each={Object.entries(props.attributes)}> <For each={Object.entries(props.attributes)}>
{([name, attr]) => ( {([name, attr]) => (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">

View File

@ -5,6 +5,7 @@ import { AttributeTooltip } from "./AttributeTooltip";
export interface TrackerViewProps { export interface TrackerViewProps {
items: () => TrackerItem[]; items: () => TrackerItem[];
onEditAttribute?: (index: number, attrName: string, attr: TrackerAttribute) => void; onEditAttribute?: (index: number, attrName: string, attr: TrackerAttribute) => void;
onClassesChange?: (index: number, classes: string[]) => void;
onRemoveClass?: (index: number, className: string) => void; onRemoveClass?: (index: number, className: string) => void;
onMoveUp?: (index: number) => void; onMoveUp?: (index: number) => void;
onMoveDown?: (index: number) => void; onMoveDown?: (index: number) => void;
@ -53,6 +54,13 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
} }
}; };
const handleClassesChange = (classes: string[]) => {
const data = editingItem();
if (data) {
props.onClassesChange?.(data.index, classes);
}
};
return ( return (
<div class="tracker-view flex-1 overflow-auto p-3 bg-white"> <div class="tracker-view flex-1 overflow-auto p-3 bg-white">
<Show <Show
@ -132,7 +140,9 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
<AttributeTooltip <AttributeTooltip
position={editingItem()!.position} position={editingItem()!.position}
attributes={props.items()[editingItem()!.index]?.attributes || {}} attributes={props.items()[editingItem()!.index]?.attributes || {}}
classes={props.items()[editingItem()!.index]?.classes || []}
onUpdate={handleUpdateAttribute} onUpdate={handleUpdateAttribute}
onClassesChange={handleClassesChange}
onClose={() => setEditingItem(null)} onClose={() => setEditingItem(null)}
/> />
</Show> </Show>

View File

@ -15,6 +15,7 @@ import {
addTrackerItem as addTracker, addTrackerItem as addTracker,
removeTrackerItem as removeTracker, removeTrackerItem as removeTracker,
updateTrackerAttribute as updateTrackerAttr, updateTrackerAttribute as updateTrackerAttr,
updateTrackerClasses as updateTrackerClassesStore,
moveTrackerItem as moveTracker, moveTrackerItem as moveTracker,
removeTrackerClass as removeClassFromTracker, removeTrackerClass as removeClassFromTracker,
getTrackerItems, getTrackerItems,
@ -255,6 +256,7 @@ export interface UseCommanderReturn {
removeTrackerItemByIndex: (index: number) => void; removeTrackerItemByIndex: (index: number) => void;
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean; 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;
moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean; moveTrackerItem: (emmet: string, direction: 'up' | 'down') => boolean;
moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void; moveTrackerItemByIndex: (index: number, direction: 'up' | 'down') => void;
removeTrackerItemClass: (emmet: string, className: string) => boolean; removeTrackerItemClass: (emmet: string, className: string) => boolean;
@ -462,6 +464,15 @@ export function useCommander(
} }
}; };
const updateTrackerClassesByIndex = (index: number, classes: 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('.') : ''}`;
updateTrackerClassesStore(emmet, classes);
}
};
const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => { const moveTrackerItem = (emmet: string, direction: 'up' | 'down') => {
return moveTracker(emmet, direction); return moveTracker(emmet, direction);
}; };
@ -535,6 +546,7 @@ export function useCommander(
removeTrackerItemByIndex, removeTrackerItemByIndex,
updateTrackerAttribute, updateTrackerAttribute,
updateTrackerAttributeByIndex, updateTrackerAttributeByIndex,
updateTrackerClassesByIndex,
moveTrackerItem, moveTrackerItem,
moveTrackerItemByIndex, moveTrackerItemByIndex,
removeTrackerItemClass, removeTrackerItemClass,

View File

@ -92,6 +92,9 @@ customElement<MdCommanderProps>(
onEditAttribute={(index, attrName, attr) => onEditAttribute={(index, attrName, attr) =>
commander.updateTrackerAttributeByIndex(index, attrName, attr) commander.updateTrackerAttributeByIndex(index, attrName, attr)
} }
onClassesChange={(index, classes) =>
commander.updateTrackerClassesByIndex(index, classes)
}
onRemoveClass={commander.removeTrackerItemClassByIndex} onRemoveClass={commander.removeTrackerItemClassByIndex}
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")} onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")} onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}

View File

@ -2,6 +2,7 @@ export {
addTrackerItem, addTrackerItem,
removeTrackerItem, removeTrackerItem,
updateTrackerAttribute, updateTrackerAttribute,
updateTrackerClasses,
moveTrackerItem, moveTrackerItem,
removeTrackerClass, removeTrackerClass,
getTrackerItems, getTrackerItems,

View File

@ -161,6 +161,29 @@ export function removeTrackerClass(emmet: string, className: string): boolean {
return true; return true;
} }
export function updateTrackerClasses(emmet: string, classes: string[]): boolean {
const index = findTrackerIndex(emmet);
if (index === -1) return false;
const item = tracker.items[index];
setTracker("items", (prev) =>
prev.map((i, idx) =>
idx === index
? { ...i, classes: [...classes] }
: i
)
);
setTracker("history", (prev) => [
...prev,
{
type: "update",
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
timestamp: new Date(),
},
]);
return true;
}
export function getTrackerItems(): TrackerItem[] { export function getTrackerItems(): TrackerItem[] {
return tracker.items; return tracker.items;
} }