refactor(editor-panel): extract LayerRow and layer crud logic
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { For, createSignal, onCleanup, onMount } from "solid-js";
|
||||
import { createSortable, maybeTransformStyle } from "@thisbeyond/solid-dnd";
|
||||
import type { DeckStore } from "../hooks/deckStore";
|
||||
import type { LayerConfig } from "../types";
|
||||
import alignLeftIcon from "./icons/align-left.png";
|
||||
import alignCenterIcon from "./icons/align-center.png";
|
||||
import alignRightIcon from "./icons/align-right.png";
|
||||
|
||||
export interface LayerRowProps {
|
||||
store: DeckStore;
|
||||
index: number;
|
||||
layer: LayerConfig;
|
||||
hovered: boolean;
|
||||
onHoverChange: (idx: number | null) => void;
|
||||
openDropdown: string | null;
|
||||
setOpenDropdown: (val: string | null) => void;
|
||||
onUpdateOrientation: (o: "n" | "s" | "e" | "w") => void;
|
||||
onUpdateFontSize: (fs?: number) => void;
|
||||
onUpdateAlign: (a?: "l" | "c" | "r") => void;
|
||||
onSelect: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const ORIENTATIONS = [
|
||||
{ value: "n" as const, label: "↑ 北" },
|
||||
{ value: "e" as const, label: "→ 东" },
|
||||
{ value: "s" as const, label: "↓ 南" },
|
||||
{ value: "w" as const, label: "← 西" },
|
||||
];
|
||||
|
||||
const ALIGNS = [
|
||||
{ value: "" as const, icon: alignCenterIcon },
|
||||
{ value: "l" as const, icon: alignLeftIcon },
|
||||
{ value: "c" as const, icon: alignCenterIcon },
|
||||
{ value: "r" as const, icon: alignRightIcon },
|
||||
];
|
||||
|
||||
const FONT_PRESETS = [3, 5, 8, 12] as const;
|
||||
|
||||
function orientChar(v: string) {
|
||||
switch (v) { case "n": return "↑"; case "e": return "→"; case "s": return "↓"; case "w": return "←"; default: return "↑"; }
|
||||
}
|
||||
|
||||
function alignSrc(v: string) {
|
||||
switch (v) { case "l": return alignLeftIcon; case "r": return alignRightIcon; default: return alignCenterIcon; }
|
||||
}
|
||||
|
||||
export function LayerRow(props: LayerRowProps) {
|
||||
const sortable = createSortable(props.index);
|
||||
const [fontOpen, setFontOpen] = createSignal(false);
|
||||
let fontRef: HTMLDivElement | undefined;
|
||||
|
||||
const selected = () => props.store.state.selectedLayer === props.index;
|
||||
|
||||
onMount(() => {
|
||||
const h = (e: MouseEvent) => { if (fontRef && !fontRef.contains(e.target as Node)) setFontOpen(false); };
|
||||
document.addEventListener("click", h);
|
||||
onCleanup(() => document.removeEventListener("click", h));
|
||||
});
|
||||
|
||||
const toggleVisible = () => {
|
||||
const fn = props.store.state.activeSide === "front"
|
||||
? props.store.actions.toggleFrontLayerVisible
|
||||
: props.store.actions.toggleBackLayerVisible;
|
||||
fn(props.index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sortable.ref}
|
||||
style={maybeTransformStyle(sortable.transform)}
|
||||
class={`flex items-center gap-1 py-1.5 px-1 relative group border-b border-gray-100 ${selected() ? "bg-blue-50" : ""}`}
|
||||
onMouseEnter={() => props.onHoverChange(props.index)}
|
||||
onMouseLeave={() => props.onHoverChange(null)}
|
||||
>
|
||||
<button {...sortable.dragActivators} class="cursor-grab text-gray-400 hover:text-gray-600 text-sm px-0.5 select-none shrink-0" title="拖动排序">⠿</button>
|
||||
|
||||
<button onClick={toggleVisible} class="shrink-0 text-sm cursor-pointer select-none" title={props.layer.visible ? "隐藏" : "显示"}>
|
||||
{props.layer.visible ? "👁" : "👁🗨"}
|
||||
</button>
|
||||
|
||||
<span class="text-sm flex-1 truncate cursor-pointer hover:text-blue-600 select-none" onClick={props.onSelect}>
|
||||
{props.layer.prop}
|
||||
</span>
|
||||
|
||||
<DropdownButton
|
||||
icon={orientChar(props.layer.orientation || "n")}
|
||||
visible={props.layer.visible}
|
||||
open={props.openDropdown === `orient-${props.index}`}
|
||||
onToggle={() => props.setOpenDropdown(props.openDropdown === `orient-${props.index}` ? null : `orient-${props.index}`)}
|
||||
title="方向"
|
||||
>
|
||||
<For each={ORIENTATIONS}>
|
||||
{(o) => <button onClick={() => props.onUpdateOrientation(o.value)} class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap">{o.label}</button>}
|
||||
</For>
|
||||
</DropdownButton>
|
||||
|
||||
<DropdownButton
|
||||
icon={<img src={alignSrc(props.layer.align || "")} alt="align" class="w-5 h-5 not-prose" />}
|
||||
visible={props.layer.visible}
|
||||
open={props.openDropdown === `align-${props.index}`}
|
||||
onToggle={() => props.setOpenDropdown(props.openDropdown === `align-${props.index}` ? null : `align-${props.index}`)}
|
||||
title="对齐"
|
||||
>
|
||||
<For each={ALIGNS}>
|
||||
{(o) => <button onClick={() => props.onUpdateAlign(o.value || undefined)} class="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"><img src={o.icon} alt="" class="w-4 h-4 not-prose max-w-none" /></button>}
|
||||
</For>
|
||||
</DropdownButton>
|
||||
|
||||
<div class="relative" ref={fontRef}>
|
||||
<button onClick={(e) => { e.stopPropagation(); setFontOpen(!fontOpen()); }}
|
||||
class={`w-7 h-7 text-xs rounded cursor-pointer flex items-center justify-center bg-gray-200 text-gray-700 hover:bg-gray-300 ${!props.layer.visible ? "invisible pointer-events-none" : ""}`}
|
||||
title="字体大小 (mm)">{props.layer.fontSize ?? 3}</button>
|
||||
{fontOpen() && (
|
||||
<div class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 p-2" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="flex items-center gap-1 mb-2">
|
||||
<input type="number" value={props.layer.fontSize ?? 3} onChange={(e) => props.onUpdateFontSize(e.target.value ? Number(e.target.value) : undefined)}
|
||||
class="w-14 text-xs px-1.5 py-1 rounded border border-gray-300" step="0.1" min="0.1" />
|
||||
<span class="text-xs text-gray-500">mm</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<For each={FONT_PRESETS}>
|
||||
{(p) => <button onClick={() => props.onUpdateFontSize(p)} class={`px-2 py-1 text-xs rounded cursor-pointer ${(props.layer.fontSize ?? 3) === p ? "bg-blue-500 text-white" : "bg-gray-100 hover:bg-gray-200 text-gray-700"}`}>{p}</button>}
|
||||
</For>
|
||||
</div>
|
||||
<button onClick={() => props.onUpdateFontSize(undefined)} class="mt-2 w-full text-xs text-gray-500 hover:text-gray-700 cursor-pointer">重置</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={(e) => { e.stopPropagation(); props.onRemove(); }}
|
||||
class={`shrink-0 w-5 h-5 text-xs rounded cursor-pointer flex items-center justify-center text-red-400 hover:text-red-600 hover:bg-red-50 transition-opacity ${props.hovered ? "opacity-100" : "opacity-0"}`}
|
||||
title="删除图层">✕</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownButton(props: {
|
||||
icon: import("solid-js").JSX.Element;
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
title: string;
|
||||
children: import("solid-js").JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
<div class="relative">
|
||||
<button onClick={(e) => { e.stopPropagation(); props.onToggle(); }}
|
||||
class={`w-7 h-7 rounded cursor-pointer flex items-center justify-center bg-gray-200 hover:bg-gray-300 ${!props.visible ? "invisible pointer-events-none" : ""}`}
|
||||
title={props.title}>{props.icon}</button>
|
||||
{props.open && <div class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10" onClick={(e) => e.stopPropagation()}>{props.children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user