refactor: layer editing ui

This commit is contained in:
hypercross 2026-03-30 11:40:45 +08:00
parent 831955e16e
commit 56cabea109
1 changed files with 195 additions and 68 deletions

View File

@ -1,4 +1,4 @@
import { For } from 'solid-js';
import { For, createSignal, onCleanup, onMount } from 'solid-js';
import type { DeckStore } from '../hooks/deckStore';
export interface LayerEditorPanelProps {
@ -13,19 +13,38 @@ const ORIENTATION_OPTIONS = [
] as const;
const ALIGN_OPTIONS = [
{ value: '', label: '对齐' },
{ value: '', label: '默认' },
{ value: 'l', label: '← 左' },
{ value: 'c', label: '≡ 中' },
{ value: 'r', label: '→ 右' }
] as const;
/**
*
*/
export function LayerEditorPanel(props: LayerEditorPanelProps) {
const { store } = props;
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 AlignIcon(value: string): string {
switch (value) {
case 'l': return '⫷';
case 'c': return '≡';
case 'r': return '⫸';
default: return '≡';
}
}
function LayerEditorPanel(props: LayerEditorPanelProps) {
const { store } = props;
const [openDropdown, setOpenDropdown] = createSignal<string | null>(null);
let dropdownRef: HTMLDivElement | undefined;
// 根据当前激活的面获取图层配置
const currentLayerConfigs = () =>
store.state.activeSide === 'front'
? store.state.frontLayerConfigs
@ -36,6 +55,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
? store.actions.updateFrontLayerConfig
: store.actions.updateBackLayerConfig;
updateFn(layerProp, { orientation });
setOpenDropdown(null);
};
const updateLayerFontSize = (layerProp: string, fontSize?: number) => {
@ -50,6 +70,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
? store.actions.updateFrontLayerConfig
: store.actions.updateBackLayerConfig;
updateFn(layerProp, { align });
setOpenDropdown(null);
};
const toggleLayerVisible = (layerProp: string) => {
@ -65,78 +86,182 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
);
};
const handleDropdownClick = (e: MouseEvent) => {
e.stopPropagation();
};
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
setOpenDropdown(null);
}
};
onMount(() => {
document.addEventListener('click', handleClickOutside);
});
onCleanup(() => {
document.removeEventListener('click', handleClickOutside);
});
const layerCount = () => currentLayerConfigs().length;
return (
<div class="w-64 flex-shrink-0">
<h3 class="font-bold mb-2 mt-0">
({store.state.activeSide === 'front' ? '正面' : '背面'})
</h3>
<div class="space-y-2">
<div ref={dropdownRef}>
<For each={currentLayerConfigs()}>
{(layer) => (
<div class="flex flex-row flex-wrap gap-1 p-2 bg-gray-50 rounded">
<div class="flex items-center gap-2">
<input
type="checkbox"
checked={layer.visible}
onChange={() => toggleLayerVisible(layer.prop)}
class="cursor-pointer"
/>
<span class="text-sm flex-1">{layer.prop}</span>
</div>
{layer.visible && (
<>
<button
onClick={() => setEditingLayer(layer.prop)}
class={`text-xs px-2 py-1 rounded cursor-pointer ${
store.state.editingLayer === layer.prop
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
{(layer, index) => (
<div
class={`flex items-center gap-1 py-1.5 px-1 ${
index() < layerCount() - 1 ? 'border-b border-gray-200' : ''
}`}
>
<input
type="checkbox"
checked={layer.visible}
onChange={() => toggleLayerVisible(layer.prop)}
class="cursor-pointer"
/>
<span class="text-sm flex-1 truncate">{layer.prop}</span>
<button
onClick={() => setEditingLayer(layer.prop)}
class={`w-7 h-7 text-xs rounded cursor-pointer flex items-center justify-center ${
!layer.visible ? 'invisible pointer-events-none' : ''
} ${
store.state.editingLayer === layer.prop
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
title="框选"
>
{store.state.editingLayer === layer.prop ? '✓' : '框'}
</button>
<div class="relative">
<button
onClick={(e) => {
e.stopPropagation();
setOpenDropdown(openDropdown() === `orient-${layer.prop}` ? null : `orient-${layer.prop}`);
}}
class={`w-7 h-7 text-sm rounded cursor-pointer flex items-center justify-center bg-gray-200 text-gray-700 hover:bg-gray-300 ${
!layer.visible ? 'invisible pointer-events-none' : ''
}`}
title="方向"
>
{OrientationIcon(layer.orientation || 'n')}
</button>
{openDropdown() === `orient-${layer.prop}` && (
<div
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10"
onClick={handleDropdownClick}
>
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
</button>
<div class="flex items-center gap-2">
<select
value={layer.orientation || 'n'}
onChange={(e) => updateLayerOrientation(layer.prop, e.target.value as 'n' | 's' | 'e' | 'w')}
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer"
>
<For each={ORIENTATION_OPTIONS}>
{(opt) => (
<option value={opt.value}>{opt.label}</option>
<For each={ORIENTATION_OPTIONS}>
{(opt) => (
<button
onClick={() => updateLayerOrientation(layer.prop, opt.value)}
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
>
{opt.label}
</button>
)}
</For>
</div>
)}
</div>
<div class="relative">
<button
onClick={(e) => {
e.stopPropagation();
setOpenDropdown(openDropdown() === `align-${layer.prop}` ? null : `align-${layer.prop}`);
}}
class={`w-7 h-7 text-sm rounded cursor-pointer flex items-center justify-center bg-gray-200 text-gray-700 hover:bg-gray-300 ${
!layer.visible ? 'invisible pointer-events-none' : ''
}`}
title="对齐"
>
{AlignIcon(layer.align || '')}
</button>
{openDropdown() === `align-${layer.prop}` && (
<div
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10"
onClick={handleDropdownClick}
>
<For each={ALIGN_OPTIONS}>
{(opt) => (
<button
onClick={() => updateLayerAlign(layer.prop, opt.value as 'l' | 'c' | 'r' | undefined || undefined)}
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
>
{opt.label}
</button>
)}
</For>
</div>
)}
</div>
<div class="relative">
<button
onClick={(e) => {
e.stopPropagation();
setOpenDropdown(openDropdown() === `font-${layer.prop}` ? null : `font-${layer.prop}`);
}}
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 ${
!layer.visible ? 'invisible pointer-events-none' : ''
}`}
title="字体大小 (mm)"
>
{layer.fontSize ?? 3}
</button>
{openDropdown() === `font-${layer.prop}` && (
<div
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 p-2"
onClick={handleDropdownClick}
>
<div class="flex items-center gap-1 mb-2">
<input
type="number"
value={layer.fontSize ?? 3}
onChange={(e) => {
const value = e.target.value;
updateLayerFontSize(layer.prop, 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"
/>
<span class="text-xs text-gray-500">mm</span>
</div>
<div class="flex gap-1">
<For each={FONT_PRESETS}>
{(preset) => (
<button
onClick={() => updateLayerFontSize(layer.prop, preset)}
class={`px-2 py-1 text-xs rounded cursor-pointer ${
(layer.fontSize ?? 3) === preset
? 'bg-blue-500 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
{preset}
</button>
)}
</For>
</select>
<select
value={layer.align || ''}
onChange={(e) => updateLayerAlign(layer.prop, e.target.value as 'l' | 'c' | 'r' | undefined || undefined)}
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer"
</div>
<button
onClick={() => updateLayerFontSize(layer.prop, undefined)}
class="mt-2 w-full text-xs text-gray-500 hover:text-gray-700 cursor-pointer"
>
<For each={ALIGN_OPTIONS}>
{(opt) => (
<option value={opt.value}>{opt.label}</option>
)}
</For>
</select>
</button>
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-gray-600">/mm</label>
<input
type="number"
value={layer.fontSize || ''}
placeholder="默认"
onChange={(e) => {
const value = e.target.value;
updateLayerFontSize(layer.prop, value ? Number(value) : undefined);
}}
class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white"
step="0.1"
min="0.1"
/>
</div>
</>
)}
)}
</div>
</div>
)}
</For>
@ -154,3 +279,5 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
</div>
);
}
export { LayerEditorPanel };