refactor: layer editing ui
This commit is contained in:
parent
831955e16e
commit
56cabea109
|
|
@ -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">
|
||||
{(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">{layer.prop}</span>
|
||||
</div>
|
||||
{layer.visible && (
|
||||
<>
|
||||
<span class="text-sm flex-1 truncate">{layer.prop}</span>
|
||||
<button
|
||||
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
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
title="框选"
|
||||
>
|
||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
||||
{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"
|
||||
|
||||
<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}
|
||||
>
|
||||
<For each={ORIENTATION_OPTIONS}>
|
||||
{(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>
|
||||
</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>
|
||||
)}
|
||||
</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) => (
|
||||
<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>
|
||||
</select>
|
||||
</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
|
||||
type="number"
|
||||
value={layer.fontSize || ''}
|
||||
placeholder="默认"
|
||||
value={layer.fontSize ?? 3}
|
||||
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"
|
||||
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>
|
||||
</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>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -154,3 +279,5 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LayerEditorPanel };
|
||||
Loading…
Reference in New Issue