refactor: attribute editing
This commit is contained in:
parent
ca48d818cc
commit
80a6b4526c
|
|
@ -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';
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue