152 lines
4.9 KiB
TypeScript
152 lines
4.9 KiB
TypeScript
import { For, createSignal, createMemo, onCleanup, onMount } from "solid-js";
|
|
import {
|
|
DragDropProvider,
|
|
DragDropSensors,
|
|
SortableProvider,
|
|
useDragDropContext,
|
|
closestCenter,
|
|
} from "@thisbeyond/solid-dnd";
|
|
import type { DeckStore } from "../hooks/deckStore";
|
|
import { LayerRow } from "./LayerRow";
|
|
|
|
export interface LayerEditorPanelProps {
|
|
store: DeckStore;
|
|
}
|
|
|
|
function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|
const { store } = props;
|
|
const [openDropdown, setOpenDropdown] = createSignal<string | null>(null);
|
|
const [hoveredIndex, setHoveredIndex] = createSignal<number | null>(null);
|
|
const [addMenuOpen, setAddMenuOpen] = createSignal(false);
|
|
let addMenuRef: HTMLDivElement | undefined;
|
|
|
|
const side = () => store.state.activeSide;
|
|
const layers = () =>
|
|
side() === "front"
|
|
? store.state.frontLayerConfigs
|
|
: store.state.backLayerConfigs;
|
|
|
|
// Stable IDs for SortableProvider — simply the indices
|
|
const ids = createMemo(() => layers().map((_, i) => i));
|
|
|
|
const updater = () =>
|
|
side() === "front"
|
|
? store.actions.updateFrontLayerConfig
|
|
: store.actions.updateBackLayerConfig;
|
|
|
|
const availableFields = createMemo(() => {
|
|
const cards = store.state.cards;
|
|
if (!cards?.length) return [];
|
|
return Object.keys(cards[0]).filter((k) => k !== "label");
|
|
});
|
|
|
|
const update = (
|
|
index: number,
|
|
patch: Partial<import("../types").LayerConfig>,
|
|
closeDropdown = false,
|
|
) => {
|
|
updater()(index, patch);
|
|
if (closeDropdown) setOpenDropdown(null);
|
|
};
|
|
|
|
onMount(() => {
|
|
const h = (e: MouseEvent) => {
|
|
if (addMenuRef && !addMenuRef.contains(e.target as Node))
|
|
setAddMenuOpen(false);
|
|
};
|
|
document.addEventListener("click", h);
|
|
onCleanup(() => document.removeEventListener("click", h));
|
|
});
|
|
|
|
return (
|
|
<div class="w-64 shrink-0">
|
|
<h3 class="font-bold mb-2 mt-0">
|
|
图层 ({side() === "front" ? "正面" : "背面"})
|
|
</h3>
|
|
|
|
<div class="relative mb-2" ref={addMenuRef}>
|
|
<button
|
|
onClick={() => setAddMenuOpen(!addMenuOpen())}
|
|
class="w-full flex items-center gap-2 px-3 py-1.5 text-sm rounded cursor-pointer border border-dashed border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-600"
|
|
>
|
|
<span class="text-lg leading-none">+</span>
|
|
<span>添加图层</span>
|
|
</button>
|
|
{addMenuOpen() && (
|
|
<div
|
|
class="absolute top-full left-0 mt-1 w-full bg-white border border-gray-300 rounded shadow-lg z-20 max-h-48 overflow-y-auto"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<For each={availableFields()}>
|
|
{(f) => (
|
|
<button
|
|
onClick={() => {
|
|
store.actions.addLayer(side(), f);
|
|
setAddMenuOpen(false);
|
|
}}
|
|
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-blue-50 hover:text-blue-600 cursor-pointer"
|
|
>
|
|
{f}
|
|
</button>
|
|
)}
|
|
</For>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DragDropProvider
|
|
onDragEnd={({ draggable, droppable }) => {
|
|
if (droppable && draggable.id !== droppable.id) {
|
|
store.actions.reorderLayers(
|
|
side(),
|
|
draggable.id as number,
|
|
droppable.id as number,
|
|
);
|
|
}
|
|
}}
|
|
collisionDetector={closestCenter}
|
|
>
|
|
<DragDropSensors>
|
|
<SortableProvider ids={ids()}>
|
|
<For each={layers()}>
|
|
{(layer, index) => (
|
|
<LayerRow
|
|
store={store}
|
|
index={index()}
|
|
layer={layer}
|
|
hovered={hoveredIndex() === index()}
|
|
onHoverChange={setHoveredIndex}
|
|
openDropdown={openDropdown()}
|
|
setOpenDropdown={setOpenDropdown}
|
|
onUpdateOrientation={(o) =>
|
|
update(index(), { orientation: o }, true)
|
|
}
|
|
onUpdateFontSize={(fs) => update(index(), { fontSize: fs })}
|
|
onUpdateAlign={(a) => update(index(), { align: a }, true)}
|
|
onSelect={() =>
|
|
store.actions.setSelectedLayer(
|
|
store.state.selectedLayer === index() ? null : index(),
|
|
)
|
|
}
|
|
onRemove={() => store.actions.removeLayer(side(), index())}
|
|
/>
|
|
)}
|
|
</For>
|
|
</SortableProvider>
|
|
</DragDropSensors>
|
|
</DragDropProvider>
|
|
|
|
<hr class="my-4" />
|
|
<button
|
|
onClick={() => store.actions.copyCode()}
|
|
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium cursor-pointer flex items-center gap-2 justify-center"
|
|
>
|
|
<span>📋</span>
|
|
<span>复制代码</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export { LayerEditorPanel };
|