import { For, createSignal, createMemo, onCleanup, onMount } from "solid-js"; import type { DeckStore } from "../hooks/deckStore"; import { DragDropProvider, DragDropSensors, createSortable, maybeTransformStyle, closestCenter, } from "@thisbeyond/solid-dnd"; import alignLeftIcon from "./icons/align-left.png"; import alignCenterIcon from "./icons/align-center.png"; import alignRightIcon from "./icons/align-right.png"; export interface LayerEditorPanelProps { store: DeckStore; } const ORIENTATION_OPTIONS = [ { value: "n", label: "↑ 北" }, { value: "e", label: "→ 东" }, { value: "s", label: "↓ 南" }, { value: "w", label: "← 西" }, ] as const; const ALIGN_OPTIONS = [ { value: "", label: "默认", icon: alignCenterIcon }, { value: "l", label: "左对齐", icon: alignLeftIcon }, { value: "c", label: "居中", icon: alignCenterIcon }, { value: "r", label: "右对齐", icon: alignRightIcon }, ] as const; const FONT_PRESETS = [3, 5, 8, 12] as const; function OrientationIcon(value: string): string { switch (value) { case "n": return "↑"; case "e": return "→"; case "s": return "↓"; case "w": return "←"; default: return "↑"; } } function AlignIconSrc(value: string): string { switch (value) { case "l": return alignLeftIcon; case "r": return alignRightIcon; default: return alignCenterIcon; } } function LayerEditorPanel(props: LayerEditorPanelProps) { const { store } = props; const [openDropdown, setOpenDropdown] = createSignal(null); const [hoveredIndex, setHoveredIndex] = createSignal(null); const [addMenuOpen, setAddMenuOpen] = createSignal(false); let addMenuRef: HTMLDivElement | undefined; const side = () => store.state.activeSide; const currentLayerConfigs = () => side() === "front" ? store.state.frontLayerConfigs : store.state.backLayerConfigs; // Available CSV fields not yet used in the current side const availableFields = createMemo(() => { const cards = store.state.cards; if (!cards || cards.length === 0) return []; const usedProps = new Set(currentLayerConfigs().map((l) => l.prop)); return Object.keys(cards[0]).filter( (k) => k !== "label" && !usedProps.has(k), ); }); const updateFn = () => side() === "front" ? store.actions.updateFrontLayerConfig : store.actions.updateBackLayerConfig; const updateLayerOrientation = ( index: number, orientation: "n" | "s" | "e" | "w", ) => { updateFn()(index, { orientation }); setOpenDropdown(null); }; const updateLayerFontSize = (index: number, fontSize?: number) => { updateFn()(index, { fontSize }); }; const updateLayerAlign = (index: number, align?: "l" | "c" | "r") => { updateFn()(index, { align }); setOpenDropdown(null); }; const selectLayer = (index: number) => { store.actions.setSelectedLayer( store.state.selectedLayer === index ? null : index, ); }; const handleClickOutside = (e: MouseEvent) => { if (addMenuRef && !addMenuRef.contains(e.target as Node)) { setAddMenuOpen(false); } }; onMount(() => { document.addEventListener("click", handleClickOutside); }); onCleanup(() => { document.removeEventListener("click", handleClickOutside); }); const onDragEnd: import("@thisbeyond/solid-dnd").DragEventHandler = ({ draggable, droppable, }) => { if (droppable) { const fromIndex = draggable.id as number; const toIndex = droppable.id as number; if (fromIndex !== toIndex) { store.actions.reorderLayers(side(), fromIndex, toIndex); } } }; return (

图层 ({side() === "front" ? "正面" : "背面"})

{/* Add layer dropdown */}
{addMenuOpen() && (
e.stopPropagation()} > {availableFields().length === 0 ? (
所有字段已添加
) : ( {(field) => ( )} )}
)}
{/* Drag and drop list */} {(layer, index) => ( updateLayerOrientation(index(), o)} updateFontSize={(fs) => updateLayerFontSize(index(), fs)} updateAlign={(a) => updateLayerAlign(index(), a)} onSelect={() => selectLayer(index())} onRemove={() => store.actions.removeLayer(side(), index())} /> )}
); } // --- Sortable layer row --- interface LayerRowProps { store: DeckStore; index: number; layer: import("../types").LayerConfig; hovered: boolean; setHoveredIndex: (idx: number | null) => void; openDropdown: string | null; setOpenDropdown: (val: string | null) => void; updateOrientation: (o: "n" | "s" | "e" | "w") => void; updateFontSize: (fs?: number) => void; updateAlign: (a?: "l" | "c" | "r") => void; onSelect: () => void; onRemove: () => void; } function LayerRow(props: LayerRowProps) { const sortable = createSortable(props.index); const [fontOpen, setFontOpen] = createSignal(false); let fontDropdownRef: HTMLDivElement | undefined; const selected = () => props.store.state.selectedLayer === props.index; const handleFontClickOutside = (e: MouseEvent) => { if (fontDropdownRef && !fontDropdownRef.contains(e.target as Node)) { setFontOpen(false); } }; onMount(() => { document.addEventListener("click", handleFontClickOutside); }); onCleanup(() => { document.removeEventListener("click", handleFontClickOutside); }); return (
props.setHoveredIndex(props.index)} onMouseLeave={() => props.setHoveredIndex(null)} > {/* Drag handle */} {/* Visibility toggle */} {/* Layer name */} {props.layer.prop} {/* Orientation */}
{props.openDropdown === `orient-${props.index}` && (
e.stopPropagation()} > {(opt) => ( )}
)}
{/* Align */}
{props.openDropdown === `align-${props.index}` && (
e.stopPropagation()} > {(opt) => ( )}
)}
{/* Font size */}
{fontOpen() && (
e.stopPropagation()} >
{ const value = e.target.value; props.updateFontSize(value ? Number(value) : undefined); }} class="w-14 text-xs px-1.5 py-1 rounded border border-gray-300" step="0.1" min="0.1" /> mm
{(preset) => ( )}
)}
{/* Delete (visible on hover) */}
); } export { LayerEditorPanel };