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'; import type { DeckStore } from '../hooks/deckStore';
export interface LayerEditorPanelProps { export interface LayerEditorPanelProps {
@ -13,19 +13,38 @@ const ORIENTATION_OPTIONS = [
] as const; ] as const;
const ALIGN_OPTIONS = [ const ALIGN_OPTIONS = [
{ value: '', label: '对齐' }, { value: '', label: '默认' },
{ value: 'l', label: '← 左' }, { value: 'l', label: '← 左' },
{ value: 'c', label: '≡ 中' }, { value: 'c', label: '≡ 中' },
{ value: 'r', label: '→ 右' } { value: 'r', label: '→ 右' }
] as const; ] as const;
/** const FONT_PRESETS = [3, 5, 8, 12] as const;
*
*/ function OrientationIcon(value: string): string {
export function LayerEditorPanel(props: LayerEditorPanelProps) { switch (value) {
const { store } = props; 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 = () => const currentLayerConfigs = () =>
store.state.activeSide === 'front' store.state.activeSide === 'front'
? store.state.frontLayerConfigs ? store.state.frontLayerConfigs
@ -36,6 +55,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
? store.actions.updateFrontLayerConfig ? store.actions.updateFrontLayerConfig
: store.actions.updateBackLayerConfig; : store.actions.updateBackLayerConfig;
updateFn(layerProp, { orientation }); updateFn(layerProp, { orientation });
setOpenDropdown(null);
}; };
const updateLayerFontSize = (layerProp: string, fontSize?: number) => { const updateLayerFontSize = (layerProp: string, fontSize?: number) => {
@ -50,6 +70,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
? store.actions.updateFrontLayerConfig ? store.actions.updateFrontLayerConfig
: store.actions.updateBackLayerConfig; : store.actions.updateBackLayerConfig;
updateFn(layerProp, { align }); updateFn(layerProp, { align });
setOpenDropdown(null);
}; };
const toggleLayerVisible = (layerProp: string) => { 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 ( return (
<div class="w-64 flex-shrink-0"> <div class="w-64 flex-shrink-0">
<h3 class="font-bold mb-2 mt-0"> <h3 class="font-bold mb-2 mt-0">
({store.state.activeSide === 'front' ? '正面' : '背面'}) ({store.state.activeSide === 'front' ? '正面' : '背面'})
</h3> </h3>
<div class="space-y-2"> <div ref={dropdownRef}>
<For each={currentLayerConfigs()}> <For each={currentLayerConfigs()}>
{(layer) => ( {(layer, index) => (
<div class="flex flex-row flex-wrap gap-1 p-2 bg-gray-50 rounded"> <div
<div class="flex items-center gap-2"> class={`flex items-center gap-1 py-1.5 px-1 ${
index() < layerCount() - 1 ? 'border-b border-gray-200' : ''
}`}
>
<input <input
type="checkbox" type="checkbox"
checked={layer.visible} checked={layer.visible}
onChange={() => toggleLayerVisible(layer.prop)} onChange={() => toggleLayerVisible(layer.prop)}
class="cursor-pointer" class="cursor-pointer"
/> />
<span class="text-sm flex-1">{layer.prop}</span> <span class="text-sm flex-1 truncate">{layer.prop}</span>
</div>
{layer.visible && (
<>
<button <button
onClick={() => setEditingLayer(layer.prop)} onClick={() => setEditingLayer(layer.prop)}
class={`text-xs px-2 py-1 rounded cursor-pointer ${ 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 store.state.editingLayer === layer.prop
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`} }`}
title="框选"
> >
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框'} {store.state.editingLayer === layer.prop ? '✓' : '框'}
</button> </button>
<div class="flex items-center gap-2">
<select <div class="relative">
value={layer.orientation || 'n'} <button
onChange={(e) => updateLayerOrientation(layer.prop, e.target.value as 'n' | 's' | 'e' | 'w')} onClick={(e) => {
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer" 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}
> >
<For each={ORIENTATION_OPTIONS}> <For each={ORIENTATION_OPTIONS}>
{(opt) => ( {(opt) => (
<option value={opt.value}>{opt.label}</option> <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> </For>
</select> </div>
<select )}
value={layer.align || ''} </div>
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 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}> <For each={ALIGN_OPTIONS}>
{(opt) => ( {(opt) => (
<option value={opt.value}>{opt.label}</option> <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> </For>
</select>
</div> </div>
<div class="flex items-center gap-2"> )}
<label class="text-xs text-gray-600">/mm</label> </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 <input
type="number" type="number"
value={layer.fontSize || ''} value={layer.fontSize ?? 3}
placeholder="默认"
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
updateLayerFontSize(layer.prop, value ? Number(value) : undefined); updateLayerFontSize(layer.prop, value ? Number(value) : undefined);
}} }}
class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white" class="w-14 text-xs px-1.5 py-1 rounded border border-gray-300"
step="0.1" step="0.1"
min="0.1" min="0.1"
/> />
<span class="text-xs text-gray-500">mm</span>
</div> </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>
</div>
<button
onClick={() => updateLayerFontSize(layer.prop, undefined)}
class="mt-2 w-full text-xs text-gray-500 hover:text-gray-700 cursor-pointer"
>
</button>
</div>
)}
</div>
</div> </div>
)} )}
</For> </For>
@ -154,3 +279,5 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
</div> </div>
); );
} }
export { LayerEditorPanel };