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';
|
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 ${
|
||||||
<input
|
index() < layerCount() - 1 ? 'border-b border-gray-200' : ''
|
||||||
type="checkbox"
|
}`}
|
||||||
checked={layer.visible}
|
>
|
||||||
onChange={() => toggleLayerVisible(layer.prop)}
|
<input
|
||||||
class="cursor-pointer"
|
type="checkbox"
|
||||||
/>
|
checked={layer.visible}
|
||||||
<span class="text-sm flex-1">{layer.prop}</span>
|
onChange={() => toggleLayerVisible(layer.prop)}
|
||||||
</div>
|
class="cursor-pointer"
|
||||||
{layer.visible && (
|
/>
|
||||||
<>
|
<span class="text-sm flex-1 truncate">{layer.prop}</span>
|
||||||
<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 ${
|
||||||
store.state.editingLayer === layer.prop
|
!layer.visible ? 'invisible pointer-events-none' : ''
|
||||||
? 'bg-blue-500 text-white'
|
} ${
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
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 ? '✓ 框选' : '框选'}
|
<For each={ORIENTATION_OPTIONS}>
|
||||||
</button>
|
{(opt) => (
|
||||||
<div class="flex items-center gap-2">
|
<button
|
||||||
<select
|
onClick={() => updateLayerOrientation(layer.prop, opt.value)}
|
||||||
value={layer.orientation || 'n'}
|
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
|
||||||
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"
|
{opt.label}
|
||||||
>
|
</button>
|
||||||
<For each={ORIENTATION_OPTIONS}>
|
)}
|
||||||
{(opt) => (
|
</For>
|
||||||
<option value={opt.value}>{opt.label}</option>
|
</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>
|
</For>
|
||||||
</select>
|
</div>
|
||||||
<select
|
<button
|
||||||
value={layer.align || ''}
|
onClick={() => updateLayerFontSize(layer.prop, undefined)}
|
||||||
onChange={(e) => updateLayerAlign(layer.prop, e.target.value as 'l' | 'c' | 'r' | undefined || undefined)}
|
class="mt-2 w-full text-xs text-gray-500 hover:text-gray-700 cursor-pointer"
|
||||||
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<For each={ALIGN_OPTIONS}>
|
重置
|
||||||
{(opt) => (
|
</button>
|
||||||
<option value={opt.value}>{opt.label}</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
)}
|
||||||
<label class="text-xs text-gray-600">字体/mm</label>
|
</div>
|
||||||
<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>
|
</For>
|
||||||
|
|
@ -154,3 +279,5 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { LayerEditorPanel };
|
||||||
Loading…
Reference in New Issue