refactor: attribute editing

This commit is contained in:
hypercross 2026-03-01 10:26:37 +08:00
parent ca48d818cc
commit 80a6b4526c
5 changed files with 197 additions and 135 deletions

View File

@ -37,7 +37,6 @@ export type {
} from './md-commander/types';
export { TabBar } from './md-commander/TabBar';
export { TrackerView } from './md-commander/TrackerView';
export { AttributeEditor } from './md-commander/AttributeEditor';
export { CommanderEntries } from './md-commander/CommanderEntries';
export { CommanderInput } from './md-commander/CommanderInput';
export { useCommander, defaultCommands } from './md-commander/hooks';

View File

@ -1,103 +0,0 @@
import { type Component, Show } from "solid-js";
import type { TrackerAttribute, TrackerAttributeType } from "./types";
export interface AttributeEditorProps {
attribute: () => TrackerAttribute;
onUpdate: (attr: TrackerAttribute) => void;
onClose: () => void;
}
export const AttributeEditor: Component<AttributeEditorProps> = (props) => {
const updateValue = (value: any) => {
props.onUpdate({
...props.attribute(),
value,
});
};
const renderEditor = () => {
const attr = props.attribute();
if (attr.type === "progress") {
const val = attr.value as { x: number; y: number };
return (
<div class="flex items-center gap-2">
<input
type="number"
value={val.x}
onChange={(e) => updateValue({ x: parseInt(e.target.value) || 0, y: val.y })}
class="w-16 px-2 py-1 border border-gray-300 rounded text-center"
min="0"
/>
<span>/</span>
<input
type="number"
value={val.y}
onChange={(e) => updateValue({ x: val.x, y: parseInt(e.target.value) || 0 })}
class="w-16 px-2 py-1 border border-gray-300 rounded text-center"
min="0"
/>
</div>
);
}
if (attr.type === "count") {
return (
<input
type="number"
value={attr.value as number}
onChange={(e) => updateValue(parseInt(e.target.value) || 0)}
class="w-24 px-2 py-1 border border-gray-300 rounded text-center"
min="0"
/>
);
}
if (attr.type === "string") {
return (
<input
type="text"
value={attr.value as string}
onChange={(e) => updateValue(e.target.value)}
class="w-full px-2 py-1 border border-gray-300 rounded"
/>
);
}
return null;
};
return (
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-4 min-w-[300px] shadow-xl">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-gray-800">{props.attribute().name}</h3>
<button
class="text-gray-500 hover:text-gray-700 text-xl"
onClick={props.onClose}
>
×
</button>
</div>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-1"></label>
<span class="inline-block px-2 py-1 bg-gray-100 rounded text-sm font-mono">
{props.attribute().type}
</span>
</div>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-1"></label>
{renderEditor()}
</div>
<div class="flex justify-end">
<button
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={props.onClose}
>
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,139 @@
import { type Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js";
import type { TrackerAttribute } from "./types";
export interface AttributeTooltipProps {
position: { x: number; y: number };
attributes: Record<string, TrackerAttribute>;
onUpdate: (attrName: string, attr: TrackerAttribute) => void;
onClose: () => void;
}
export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
const [position, setPosition] = createSignal(props.position);
// 点击外部关闭
createEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest(".attribute-tooltip")) {
props.onClose();
}
};
document.addEventListener("click", handleClickOutside);
onCleanup(() => document.removeEventListener("click", handleClickOutside));
});
// 处理属性更新
const handleUpdate = (attrName: string, value: any) => {
const attr = props.attributes[attrName];
props.onUpdate(attrName, { ...attr, value });
};
// 渲染不同类型的属性编辑器
const renderEditor = (attrName: string, attr: TrackerAttribute) => {
if (attr.type === "progress") {
const val = attr.value as { x: number; y: number };
const percentage = val.y > 0 ? (val.x / val.y) * 100 : 0;
return (
<div class="flex flex-col gap-1">
{/* 进度条 */}
<div class="w-full h-4 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-200"
style={{ width: `${Math.min(100, percentage)}%` }}
/>
</div>
{/* 输入框 */}
<div class="flex items-center gap-1">
<input
type="number"
value={val.x}
onChange={(e) => handleUpdate(attrName, { x: parseInt(e.target.value) || 0, y: val.y })}
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
min="0"
/>
<span class="text-gray-500">/</span>
<input
type="number"
value={val.y}
onChange={(e) => handleUpdate(attrName, { x: val.x, y: parseInt(e.target.value) || 0 })}
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
min="0"
/>
</div>
</div>
);
}
if (attr.type === "count") {
const value = attr.value as number;
return (
<div class="flex items-center gap-1">
<button
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
onClick={() => handleUpdate(attrName, value - 1)}
>
</button>
<input
type="number"
value={value}
onChange={(e) => handleUpdate(attrName, parseInt(e.target.value) || 0)}
class="w-12 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
min="0"
/>
<button
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
onClick={() => handleUpdate(attrName, value + 1)}
>
+
</button>
</div>
);
}
if (attr.type === "string") {
return (
<input
type="text"
value={attr.value as string}
onChange={(e) => handleUpdate(attrName, e.target.value)}
class="w-full px-2 py-1 border border-gray-300 rounded text-sm"
/>
);
}
return <span class="text-gray-500"></span>;
};
return (
<div
class="attribute-tooltip fixed z-50 bg-white border border-gray-300 rounded-lg shadow-xl p-3 min-w-[150px]"
style={{
left: `${position().x}px`,
top: `${position().y}px`,
}}
onClick={(e) => e.stopPropagation()}
>
<div class="flex items-center justify-between mb-2 pb-2 border-b border-gray-200">
<span class="font-bold text-gray-700 text-sm"></span>
<button
class="text-gray-400 hover:text-gray-600 text-lg leading-none"
onClick={props.onClose}
>
×
</button>
</div>
<div class="space-y-3">
<For each={Object.entries(props.attributes)}>
{([name, attr]) => (
<div class="flex flex-col gap-1">
<label class="text-xs text-gray-500 font-medium">{name}</label>
{renderEditor(name, attr)}
</div>
)}
</For>
</div>
</div>
);
};

View File

@ -1,5 +1,6 @@
import { type Component, For, Show, Index } from "solid-js";
import { type Component, For, Show, createSignal } from "solid-js";
import type { TrackerItem, TrackerAttribute } from "./types";
import { AttributeTooltip } from "./AttributeTooltip";
export interface TrackerViewProps {
items: () => TrackerItem[];
@ -11,6 +12,11 @@ export interface TrackerViewProps {
}
export const TrackerView: Component<TrackerViewProps> = (props) => {
const [editingItem, setEditingItem] = createSignal<{
index: number;
position: { x: number; y: number };
} | null>(null);
const formatAttributeValue = (attr: TrackerAttribute): string => {
if (attr.type === "progress") {
const val = attr.value as { x: number; y: number };
@ -27,6 +33,26 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
return `${name}=${val}`;
};
const handleAttributeClick = (e: MouseEvent, index: number, attrName: string, attr: TrackerAttribute) => {
e.preventDefault();
e.stopPropagation();
const rect = (e.target as HTMLElement).getBoundingClientRect();
setEditingItem({
index,
position: {
x: rect.left,
y: rect.bottom + 5,
},
});
};
const handleUpdateAttribute = (attrName: string, attr: TrackerAttribute) => {
const data = editingItem();
if (data) {
props.onEditAttribute?.(data.index, attrName, attr);
}
};
return (
<div class="tracker-view flex-1 overflow-auto p-3 bg-white">
<Show
@ -49,10 +75,25 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
<Show when={item.classes.length > 0}>
<span class="text-xs text-blue-600 font-mono whitespace-nowrap">.{item.classes.join(".")}</span>
</Show>
{/* 属性简写 */}
{/* 属性链接 - 可点击编辑 */}
<Show when={Object.keys(item.attributes).length > 0}>
<span class="text-xs text-gray-500 font-mono whitespace-nowrap truncate">
[{Object.entries(item.attributes).map(([k, v]) => formatAttributeShort(k, v)).join(" ")}]
<span class="text-xs font-mono whitespace-nowrap truncate">
[
<For each={Object.entries(item.attributes)}>
{([name, attr], attrIndex) => (
<>
<Show when={attrIndex() > 0}> </Show>
<button
class="text-gray-500 hover:text-blue-600 hover:underline cursor-pointer transition-colors"
title={`点击编辑 ${name}`}
onClick={(e) => handleAttributeClick(e, index(), name, attr)}
>
{name}={formatAttributeValue(attr)}
</button>
</>
)}
</For>
]
</span>
</Show>
</div>
@ -85,6 +126,16 @@ export const TrackerView: Component<TrackerViewProps> = (props) => {
</For>
</div>
</Show>
{/* 属性编辑工具提示 */}
<Show when={editingItem()}>
<AttributeTooltip
position={editingItem()!.position}
attributes={props.items()[editingItem()!.index]?.attributes || {}}
onUpdate={handleUpdateAttribute}
onClose={() => setEditingItem(null)}
/>
</Show>
</div>
);
};

View File

@ -1,11 +1,10 @@
import { customElement, noShadowDOM } from "solid-element";
import { onMount, onCleanup, createSignal, Show } from "solid-js";
import { onMount, onCleanup, Show } from "solid-js";
import { useCommander } from "./hooks";
import { CommanderInput } from "./CommanderInput";
import { CommanderEntries } from "./CommanderEntries";
import { TrackerView } from "./TrackerView";
import { TabBar } from "./TabBar";
import { AttributeEditor } from "./AttributeEditor";
import type { MdCommanderProps, TrackerAttribute } from "./types";
customElement<MdCommanderProps>(
@ -15,11 +14,6 @@ customElement<MdCommanderProps>(
noShadowDOM();
const commander = useCommander(props.commands);
const [editingAttr, setEditingAttr] = createSignal<{
index: number;
attrName: string;
attr: TrackerAttribute;
} | null>(null);
const handleKeyDown = (e: KeyboardEvent) => {
if (commander.showCompletions() && commander.completions().length > 0) {
@ -43,11 +37,7 @@ customElement<MdCommanderProps>(
return;
}
if (e.key === "Escape") {
if (editingAttr()) {
setEditingAttr(null);
} else {
commander.setShowCompletions(false);
}
return;
}
}
@ -100,7 +90,7 @@ customElement<MdCommanderProps>(
<TrackerView
items={commander.trackerItems}
onEditAttribute={(index, attrName, attr) =>
setEditingAttr({ index, attrName, attr })
commander.updateTrackerAttributeByIndex(index, attrName, attr)
}
onRemoveClass={commander.removeTrackerItemClassByIndex}
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
@ -138,20 +128,6 @@ customElement<MdCommanderProps>(
onAcceptCompletion={commander.acceptCompletion}
/>
</div>
{/* 属性编辑器弹窗 */}
<Show when={editingAttr()}>
<AttributeEditor
attribute={() => editingAttr()!.attr}
onUpdate={(attr) => {
const data = editingAttr();
if (data) {
commander.updateTrackerAttributeByIndex(data.index, data.attrName, attr);
}
}}
onClose={() => setEditingAttr(null)}
/>
</Show>
</div>
);
},