Compare commits
8 Commits
18ea01b904
...
cd5741692a
| Author | SHA1 | Date |
|---|---|---|
|
|
cd5741692a | |
|
|
d8058fd224 | |
|
|
49fca8c18f | |
|
|
ceb2da8b1a | |
|
|
56cabea109 | |
|
|
831955e16e | |
|
|
ea57cf8d2b | |
|
|
273f949839 |
|
|
@ -104,8 +104,8 @@
|
|||
```
|
||||
|
||||
**功能:**
|
||||
- 点击骰子图标执行掷骰
|
||||
- 点击文本重置为公式
|
||||
- 点击文本执行掷骰
|
||||
- 点击骰子图标重置为公式
|
||||
- `key` 属性将结果记录到 URL 参数中 (`?dice-attack=15`)
|
||||
|
||||
**属性:**
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import {createMemo, For} from 'solid-js';
|
||||
import {parseMarkdown} from '../../markdown';
|
||||
import { createMemo, For, Show } from 'solid-js';
|
||||
import { parseMarkdown } from '../../markdown';
|
||||
import { getLayerStyle } from './hooks/dimensions';
|
||||
import type { CardData, CardSide } from './types';
|
||||
import {DeckStore} from "./hooks/deckStore";
|
||||
import {processVariables} from "../utils/csv-loader";
|
||||
import {resolvePath} from "../utils/path";
|
||||
import type { CardData, CardSide, LayerConfig } from './types';
|
||||
import { DeckStore } from "./hooks/deckStore";
|
||||
import { processVariables } from "../utils/csv-loader";
|
||||
import { resolvePath } from "../utils/path";
|
||||
import type { LayerInteractionHandlers } from './hooks/useLayerInteraction';
|
||||
|
||||
export interface CardLayerProps {
|
||||
cardData: CardData;
|
||||
store: DeckStore;
|
||||
side?: CardSide;
|
||||
interaction?: LayerInteractionHandlers;
|
||||
}
|
||||
|
||||
export function CardLayer(props: CardLayerProps) {
|
||||
|
|
@ -20,36 +22,160 @@ export function CardLayer(props: CardLayerProps) {
|
|||
: props.store.state.backLayerConfigs.filter((l) => l.visible)
|
||||
);
|
||||
const dimensions = () => props.store.state.dimensions!;
|
||||
const showBounds = () => props.store.state.isEditing;
|
||||
const selectedLayer = () => props.store.state.selectedLayer;
|
||||
const draggingState = () => props.store.state.draggingState;
|
||||
|
||||
function renderLayerContent(content: string) {
|
||||
const iconPath = resolvePath(props.store.state.cards.sourcePath, props.cardData.iconPath ?? "./assets");
|
||||
return parseMarkdown(processVariables(content, props.cardData, props.store.state.cards), iconPath) as string;
|
||||
}
|
||||
|
||||
const getAlignStyle = (align?: 'l' | 'c' | 'r') => {
|
||||
if (align === 'l') return 'left';
|
||||
if (align === 'r') return 'right';
|
||||
return 'center';
|
||||
};
|
||||
|
||||
const isLayerSelected = (layer: LayerConfig) => selectedLayer() === layer.prop;
|
||||
|
||||
const getFrameBounds = (layer: LayerConfig) => {
|
||||
const dims = dimensions();
|
||||
const left = (layer.x1 - 1) * dims.cellWidth;
|
||||
const top = (layer.y1 - 1) * dims.cellHeight;
|
||||
const width = (layer.x2 - layer.x1 + 1) * dims.cellWidth;
|
||||
const height = (layer.y2 - layer.y1 + 1) * dims.cellHeight;
|
||||
return { left, top, width, height };
|
||||
};
|
||||
|
||||
const handleLayerClick = (layerProp: string, e: MouseEvent) => {
|
||||
if (props.interaction) {
|
||||
props.interaction.onLayerClick(layerProp, e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrameMouseDown = (
|
||||
action: 'drag' | 'resize-corner' | 'resize-edge',
|
||||
anchor?: 'nw' | 'ne' | 'sw' | 'se',
|
||||
edge?: 'n' | 's' | 'e' | 'w',
|
||||
e?: MouseEvent
|
||||
) => {
|
||||
if (props.interaction) {
|
||||
props.interaction.onFrameMouseDown(action, anchor, edge, e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<For each={layers()}>
|
||||
{(layer) => {
|
||||
const bounds = () => getFrameBounds(layer);
|
||||
const isSelected = () => isLayerSelected(layer);
|
||||
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
class="absolute flex flex-col items-center justify-center text-center prose prose-sm"
|
||||
<article
|
||||
class="absolute flex flex-col items-stretch justify-center prose prose-sm cursor-pointer"
|
||||
classList={{
|
||||
'ring-2 ring-blue-500 ring-offset-1': isSelected() && !draggingState()
|
||||
}}
|
||||
style={{
|
||||
...getLayerStyle(layer, dimensions()),
|
||||
'font-size': `${layer.fontSize || 3}mm`
|
||||
'font-size': `${layer.fontSize || 3}mm`,
|
||||
'text-align': getAlignStyle(layer.align)
|
||||
}}
|
||||
innerHTML={renderLayerContent(props.cardData[layer.prop])}
|
||||
onClick={(e) => handleLayerClick(layer.prop, e)}
|
||||
/>
|
||||
{showBounds() && (
|
||||
<Show when={isSelected()}>
|
||||
<div
|
||||
class="absolute border-2 border-blue-500/50 pointer-events-none select-none"
|
||||
class="absolute border-2 border-blue-500 pointer-events-none z-10"
|
||||
style={{
|
||||
left: `${(layer.x1 - 1) * dimensions().cellWidth}mm`,
|
||||
top: `${(layer.y1 - 1) * dimensions().cellHeight}mm`,
|
||||
width: `${(layer.x2 - layer.x1 + 1) * dimensions().cellWidth}mm`,
|
||||
height: `${(layer.y2 - layer.y1 + 1) * dimensions().cellHeight}mm`
|
||||
left: `${bounds().left}mm`,
|
||||
top: `${bounds().top}mm`,
|
||||
width: `${bounds().width}mm`,
|
||||
height: `${bounds().height}mm`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
class="absolute pointer-events-auto z-10"
|
||||
style={{
|
||||
left: `${bounds().left}mm`,
|
||||
top: `${bounds().top}mm`,
|
||||
width: `${bounds().width}mm`,
|
||||
height: `${bounds().height}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('drag', undefined, undefined, e)}
|
||||
/>
|
||||
<Show when={!draggingState() || draggingState()?.action !== 'drag'}>
|
||||
<div
|
||||
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-20"
|
||||
style={{
|
||||
left: `${bounds().left - 2}mm`,
|
||||
top: `${bounds().top - 2}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'nw', undefined, e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-20"
|
||||
style={{
|
||||
left: `${bounds().left + bounds().width - 2}mm`,
|
||||
top: `${bounds().top - 2}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'ne', undefined, e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-20"
|
||||
style={{
|
||||
left: `${bounds().left - 2}mm`,
|
||||
top: `${bounds().top + bounds().height - 2}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'sw', undefined, e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-20"
|
||||
style={{
|
||||
left: `${bounds().left + bounds().width - 2}mm`,
|
||||
top: `${bounds().top + bounds().height - 2}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'se', undefined, e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute h-2 cursor-ns-resize pointer-events-auto z-10"
|
||||
style={{
|
||||
left: `${bounds().left}mm`,
|
||||
top: `${bounds().top - 1}mm`,
|
||||
width: `${bounds().width}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'n', e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute h-2 cursor-ns-resize pointer-events-auto z-10"
|
||||
style={{
|
||||
left: `${bounds().left}mm`,
|
||||
top: `${bounds().top + bounds().height - 1}mm`,
|
||||
width: `${bounds().width}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 's', e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute w-2 cursor-ew-resize pointer-events-auto z-10"
|
||||
style={{
|
||||
left: `${bounds().left - 1}mm`,
|
||||
top: `${bounds().top}mm`,
|
||||
height: `${bounds().height}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'w', e)}
|
||||
/>
|
||||
<div
|
||||
class="absolute w-2 cursor-ew-resize pointer-events-auto z-10"
|
||||
style={{
|
||||
left: `${bounds().left + bounds().width - 1}mm`,
|
||||
top: `${bounds().top}mm`,
|
||||
height: `${bounds().height}mm`
|
||||
}}
|
||||
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'e', e)}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,67 @@
|
|||
import { Show, For, createMemo } from 'solid-js';
|
||||
import { useCardSelection } from './hooks/useCardSelection';
|
||||
import { getSelectionBoxStyle } from './hooks/useCardSelection';
|
||||
import { Show, For, createMemo, onCleanup, onMount } from 'solid-js';
|
||||
import { getShapeClipPath } from './hooks/shape-styles';
|
||||
import { CardLayer } from './CardLayer';
|
||||
import type { DeckStore } from './hooks/deckStore';
|
||||
import { useLayerInteraction } from './hooks/useLayerInteraction';
|
||||
|
||||
export interface CardPreviewProps {
|
||||
store: DeckStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡牌预览组件
|
||||
*/
|
||||
export function CardPreview(props: CardPreviewProps) {
|
||||
const { store } = props;
|
||||
|
||||
const currentCard = createMemo(() => store.state.cards[store.state.activeTab]);
|
||||
const selectionStyle = createMemo(() =>
|
||||
getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions)
|
||||
);
|
||||
const shapeClipPath = createMemo(() => {
|
||||
const dims = store.state.dimensions;
|
||||
if (!dims) return 'none';
|
||||
return getShapeClipPath(store.state.shape, dims.cardWidth, dims.cardHeight, store.state.cornerRadius);
|
||||
});
|
||||
|
||||
const selection = useCardSelection(store);
|
||||
const interaction = useLayerInteraction(store);
|
||||
|
||||
let cardRef: HTMLDivElement | undefined;
|
||||
|
||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||
if (cardRef && store.state.draggingState) {
|
||||
interaction.onCardMouseMove(e, cardRef);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalMouseUp = () => {
|
||||
if (store.state.draggingState) {
|
||||
interaction.onCardMouseUp();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('mousemove', handleGlobalMouseMove);
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener('mousemove', handleGlobalMouseMove);
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex justify-center drop-shadow">
|
||||
<Show when={store.state.activeTab < store.state.cards.length}>
|
||||
<div
|
||||
ref={cardRef}
|
||||
class="relative bg-white border border-gray-300 overflow-hidden"
|
||||
classList={{ 'select-none': store.state.isEditing }}
|
||||
classList={{ 'select-none': !!store.state.draggingState }}
|
||||
style={{
|
||||
width: `${store.state.dimensions?.cardWidth}mm`,
|
||||
height: `${store.state.dimensions?.cardHeight}mm`,
|
||||
'clip-path': shapeClipPath() !== 'none' ? shapeClipPath() : undefined
|
||||
}}
|
||||
onMouseDown={(e) => selection.onMouseDown(e, cardRef!)}
|
||||
onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
|
||||
onMouseUp={selection.onMouseUp}
|
||||
onMouseLeave={selection.onMouseLeave}
|
||||
onClick={(e) => {
|
||||
if (!store.state.draggingState && cardRef) {
|
||||
interaction.onCardClick(e, cardRef);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show when={store.state.isSelecting && selectionStyle()}>
|
||||
<div
|
||||
class="absolute bg-blue-500/30 border-2 border-blue-500 pointer-events-none"
|
||||
style={selectionStyle()!}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class="absolute"
|
||||
style={{
|
||||
|
|
@ -87,6 +96,7 @@ export function CardPreview(props: CardPreviewProps) {
|
|||
cardData={currentCard()}
|
||||
store={store}
|
||||
side={store.state.activeSide}
|
||||
interaction={interaction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { For } from 'solid-js';
|
||||
import { For, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import type { DeckStore } from '../hooks/deckStore';
|
||||
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;
|
||||
|
|
@ -12,13 +15,38 @@ const ORIENTATION_OPTIONS = [
|
|||
{ value: 'w', label: '← 西' }
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 图层编辑面板:图层可见性切换、位置编辑、复制代码
|
||||
*/
|
||||
export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||
const { store } = props;
|
||||
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<string | null>(null);
|
||||
let dropdownRef: HTMLDivElement | undefined;
|
||||
|
||||
// 根据当前激活的面获取图层配置
|
||||
const currentLayerConfigs = () =>
|
||||
store.state.activeSide === 'front'
|
||||
? store.state.frontLayerConfigs
|
||||
|
|
@ -29,6 +57,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
? store.actions.updateFrontLayerConfig
|
||||
: store.actions.updateBackLayerConfig;
|
||||
updateFn(layerProp, { orientation });
|
||||
setOpenDropdown(null);
|
||||
};
|
||||
|
||||
const updateLayerFontSize = (layerProp: string, fontSize?: number) => {
|
||||
|
|
@ -38,6 +67,14 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
updateFn(layerProp, { fontSize });
|
||||
};
|
||||
|
||||
const updateLayerAlign = (layerProp: string, align?: 'l' | 'c' | 'r') => {
|
||||
const updateFn = store.state.activeSide === 'front'
|
||||
? store.actions.updateFrontLayerConfig
|
||||
: store.actions.updateBackLayerConfig;
|
||||
updateFn(layerProp, { align });
|
||||
setOpenDropdown(null);
|
||||
};
|
||||
|
||||
const toggleLayerVisible = (layerProp: string) => {
|
||||
const toggleFn = store.state.activeSide === 'front'
|
||||
? store.actions.toggleFrontLayerVisible
|
||||
|
|
@ -45,73 +82,181 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
toggleFn(layerProp);
|
||||
};
|
||||
|
||||
const setEditingLayer = (layerProp: string) => {
|
||||
store.actions.setEditingLayer(
|
||||
store.state.editingLayer === layerProp ? null : layerProp
|
||||
const selectLayer = (layerProp: string) => {
|
||||
store.actions.setSelectedLayer(
|
||||
store.state.selectedLayer === layerProp ? null : layerProp
|
||||
);
|
||||
};
|
||||
|
||||
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' : ''
|
||||
} ${
|
||||
store.state.selectedLayer === layer.prop ? 'bg-blue-50' : ''
|
||||
}`}
|
||||
>
|
||||
<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'
|
||||
}`}
|
||||
<span
|
||||
class="text-sm flex-1 truncate cursor-pointer hover:text-blue-600"
|
||||
onClick={() => selectLayer(layer.prop)}
|
||||
>
|
||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
||||
{layer.prop}
|
||||
</span>
|
||||
<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>
|
||||
<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"
|
||||
{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>
|
||||
</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() === `align-${layer.prop}` ? null : `align-${layer.prop}`);
|
||||
}}
|
||||
class={`w-7 h-7 rounded cursor-pointer flex items-center justify-center bg-gray-200 hover:bg-gray-300 ${
|
||||
!layer.visible ? 'invisible pointer-events-none' : ''
|
||||
}`}
|
||||
title="对齐"
|
||||
>
|
||||
<img src={AlignIconSrc(layer.align || '')} alt="align" class="w-5 h-5 not-prose" />
|
||||
</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="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
<img src={opt.icon} alt="" class="w-4 h-4 not-prose max-w-none" />
|
||||
</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 || ''}
|
||||
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>
|
||||
|
|
@ -129,3 +274,5 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LayerEditorPanel };
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1002 B |
Binary file not shown.
|
After Width: | Height: | Size: 1006 B |
Binary file not shown.
|
After Width: | Height: | Size: 996 B |
|
|
@ -1,12 +1,9 @@
|
|||
import { createStore } from 'solid-js/store';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { calculateDimensions } from './dimensions';
|
||||
import { loadCSV, CSV } from '../../utils/csv-loader';
|
||||
import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser';
|
||||
import type { CardData, LayerConfig, Dimensions, CardSide, CardShape } from '../types';
|
||||
|
||||
/**
|
||||
* 默认配置常量
|
||||
*/
|
||||
export const DECK_DEFAULTS = {
|
||||
SIZE_W: 54,
|
||||
SIZE_H: 86,
|
||||
|
|
@ -17,8 +14,17 @@ export const DECK_DEFAULTS = {
|
|||
CORNER_RADIUS: 3
|
||||
} as const;
|
||||
|
||||
export interface DraggingState {
|
||||
layer: string;
|
||||
action: 'drag' | 'resize-corner' | 'resize-edge';
|
||||
anchor?: 'nw' | 'ne' | 'sw' | 'se';
|
||||
edge?: 'n' | 's' | 'e' | 'w';
|
||||
startX: number;
|
||||
startY: number;
|
||||
startGrid: { x1: number; y1: number; x2: number; y2: number };
|
||||
}
|
||||
|
||||
export interface DeckState {
|
||||
// 基本属性
|
||||
sizeW: number;
|
||||
sizeH: number;
|
||||
gridW: number;
|
||||
|
|
@ -29,41 +35,33 @@ export interface DeckState {
|
|||
shape: CardShape;
|
||||
fixed: boolean;
|
||||
src: string;
|
||||
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
||||
rawSrc: string;
|
||||
|
||||
// 解析后的尺寸
|
||||
dimensions: Dimensions | null;
|
||||
|
||||
// 卡牌数据
|
||||
cards: CSV<CardData>;
|
||||
activeTab: number;
|
||||
|
||||
// 图层配置
|
||||
frontLayerConfigs: LayerConfig[];
|
||||
backLayerConfigs: LayerConfig[];
|
||||
|
||||
// 编辑状态
|
||||
isEditing: boolean;
|
||||
editingLayer: string | null;
|
||||
selectedLayer: string | null;
|
||||
activeSide: CardSide;
|
||||
|
||||
// 框选状态
|
||||
isSelecting: boolean;
|
||||
selectStart: { x: number; y: number } | null;
|
||||
selectEnd: { x: number; y: number } | null;
|
||||
|
||||
// 加载状态
|
||||
isLoading: boolean;
|
||||
draggingState: DraggingState | null;
|
||||
|
||||
// 错误状态
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// 导出状态
|
||||
isExporting: boolean;
|
||||
exportProgress: number; // 0-100
|
||||
exportProgress: number;
|
||||
exportError: string | null;
|
||||
|
||||
// 打印设置
|
||||
printOrientation: 'portrait' | 'landscape';
|
||||
printFrontOddPageOffsetX: number;
|
||||
printFrontOddPageOffsetY: number;
|
||||
|
|
@ -71,7 +69,6 @@ export interface DeckState {
|
|||
}
|
||||
|
||||
export interface DeckActions {
|
||||
// 基本属性设置
|
||||
setSizeW: (size: number) => void;
|
||||
setSizeH: (size: number) => void;
|
||||
setGridW: (grid: number) => void;
|
||||
|
|
@ -81,50 +78,45 @@ export interface DeckActions {
|
|||
setCornerRadius: (cornerRadius: number) => void;
|
||||
setShape: (shape: CardShape) => void;
|
||||
|
||||
// 数据设置
|
||||
setCards: (cards: CSV<CardData>) => void;
|
||||
setActiveTab: (index: number) => void;
|
||||
updateCardData: (index: number, key: string, value: string) => void;
|
||||
|
||||
// 图层操作 - 正面
|
||||
setFrontLayerConfigs: (configs: LayerConfig[]) => void;
|
||||
updateFrontLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
||||
toggleFrontLayerVisible: (prop: string) => void;
|
||||
|
||||
// 图层操作 - 背面
|
||||
setBackLayerConfigs: (configs: LayerConfig[]) => void;
|
||||
updateBackLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
||||
toggleBackLayerVisible: (prop: string) => void;
|
||||
|
||||
// 编辑状态
|
||||
setIsEditing: (editing: boolean) => void;
|
||||
setEditingLayer: (layer: string | null) => void;
|
||||
updateLayerPosition: (x1: number, y1: number, x2: number, y2: number) => void;
|
||||
setSelectedLayer: (layer: string | null) => void;
|
||||
setActiveSide: (side: CardSide) => void;
|
||||
|
||||
// 框选操作
|
||||
setIsSelecting: (selecting: boolean) => void;
|
||||
setSelectStart: (pos: { x: number; y: number } | null) => void;
|
||||
setSelectEnd: (pos: { x: number; y: number } | null) => void;
|
||||
cancelSelection: () => void;
|
||||
|
||||
// 数据加载
|
||||
setDraggingState: (state: DraggingState | null) => void;
|
||||
moveLayer: (layerProp: string, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||
resizeLayerCorner: (layerProp: string, anchor: 'nw' | 'ne' | 'sw' | 'se', dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||
resizeLayerEdge: (layerProp: string, edge: 'n' | 's' | 'e' | 'w', delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||
|
||||
loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string, backLayersStr?: string) => Promise<void>;
|
||||
setError: (error: string | null) => void;
|
||||
clearError: () => void;
|
||||
|
||||
// 生成代码
|
||||
generateCode: (backLayersStr?: string) => string;
|
||||
copyCode: (backLayersStr?: string) => Promise<void>;
|
||||
|
||||
// 导出操作
|
||||
setExporting: (exporting: boolean) => void;
|
||||
exportDeck: () => void;
|
||||
setExportProgress: (progress: number) => void;
|
||||
setExportError: (error: string | null) => void;
|
||||
clearExportError: () => void;
|
||||
|
||||
// 打印设置
|
||||
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
||||
setPrintFrontOddPageOffsetX: (offset: number) => void;
|
||||
setPrintFrontOddPageOffsetY: (offset: number) => void;
|
||||
|
|
@ -136,9 +128,6 @@ export interface DeckStore {
|
|||
actions: DeckActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 deck store
|
||||
*/
|
||||
export function createDeckStore(
|
||||
initialSrc: string = '',
|
||||
): DeckStore {
|
||||
|
|
@ -160,11 +149,12 @@ export function createDeckStore(
|
|||
frontLayerConfigs: [],
|
||||
backLayerConfigs: [],
|
||||
isEditing: false,
|
||||
editingLayer: null,
|
||||
selectedLayer: null,
|
||||
activeSide: 'front',
|
||||
isSelecting: false,
|
||||
selectStart: null,
|
||||
selectEnd: null,
|
||||
draggingState: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isExporting: false,
|
||||
|
|
@ -176,7 +166,6 @@ export function createDeckStore(
|
|||
printDoubleSided: false
|
||||
});
|
||||
|
||||
// 更新尺寸并重新计算 dimensions
|
||||
const updateDimensions = () => {
|
||||
const dims = calculateDimensions({
|
||||
sizeW: state.sizeW,
|
||||
|
|
@ -226,7 +215,6 @@ export function createDeckStore(
|
|||
setState('cards', index, key, value);
|
||||
};
|
||||
|
||||
// 正面图层操作
|
||||
const setFrontLayerConfigs = (configs: LayerConfig[]) => setState({ frontLayerConfigs: configs });
|
||||
const updateFrontLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
||||
setState('frontLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
||||
|
|
@ -237,7 +225,6 @@ export function createDeckStore(
|
|||
));
|
||||
};
|
||||
|
||||
// 背面图层操作
|
||||
const setBackLayerConfigs = (configs: LayerConfig[]) => setState({ backLayerConfigs: configs });
|
||||
const updateBackLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
||||
setState('backLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
||||
|
|
@ -249,21 +236,9 @@ export function createDeckStore(
|
|||
};
|
||||
|
||||
const setIsEditing = (editing: boolean) => setState({ isEditing: editing });
|
||||
const setEditingLayer = (layer: string | null) => setState({ editingLayer: layer });
|
||||
const setSelectedLayer = (layer: string | null) => setState({ selectedLayer: layer });
|
||||
const setActiveSide = (side: CardSide) => setState({ activeSide: side });
|
||||
|
||||
const updateLayerPosition = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
const layer = state.editingLayer;
|
||||
if (!layer) return;
|
||||
const currentSide = state.activeSide;
|
||||
const configs = currentSide === 'front' ? state.frontLayerConfigs : state.backLayerConfigs;
|
||||
const setter = currentSide === 'front' ? setFrontLayerConfigs : setBackLayerConfigs;
|
||||
setter(configs.map((config) =>
|
||||
config.prop === layer ? { ...config, x1, y1, x2, y2 } : config
|
||||
));
|
||||
setState({ editingLayer: null });
|
||||
};
|
||||
|
||||
const setIsSelecting = (selecting: boolean) => setState({ isSelecting: selecting });
|
||||
const setSelectStart = (pos: { x: number; y: number } | null) => setState({ selectStart: pos });
|
||||
const setSelectEnd = (pos: { x: number; y: number } | null) => setState({ selectEnd: pos });
|
||||
|
|
@ -271,7 +246,68 @@ export function createDeckStore(
|
|||
setState({ isSelecting: false, selectStart: null, selectEnd: null });
|
||||
};
|
||||
|
||||
// 加载卡牌数据(核心逻辑)
|
||||
const setDraggingState = (draggingState: DraggingState | null) => setState({ draggingState });
|
||||
|
||||
const getLayerConfig = (layerProp: string): LayerConfig | undefined => {
|
||||
const configs = state.activeSide === 'front' ? state.frontLayerConfigs : state.backLayerConfigs;
|
||||
return configs.find(c => c.prop === layerProp);
|
||||
};
|
||||
|
||||
const updateLayerConfig = (layerProp: string, updates: Partial<LayerConfig>) => {
|
||||
if (state.activeSide === 'front') {
|
||||
updateFrontLayerConfig(layerProp, updates);
|
||||
} else {
|
||||
updateBackLayerConfig(layerProp, updates);
|
||||
}
|
||||
};
|
||||
|
||||
const moveLayer = (layerProp: string, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||
const layer = getLayerConfig(layerProp);
|
||||
if (!layer) return;
|
||||
|
||||
const grid = startGrid ?? { x1: layer.x1, y1: layer.y1, x2: layer.x2, y2: layer.y2 };
|
||||
|
||||
updateLayerConfig(layerProp, {
|
||||
x1: grid.x1 + dxGrid,
|
||||
x2: grid.x2 + dxGrid,
|
||||
y1: grid.y1 + dyGrid,
|
||||
y2: grid.y2 + dyGrid
|
||||
});
|
||||
};
|
||||
|
||||
const resizeLayerCorner = (layerProp: string, anchor: 'nw' | 'ne' | 'sw' | 'se', dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||
const layer = getLayerConfig(layerProp);
|
||||
if (!layer) return;
|
||||
|
||||
const updates: Partial<LayerConfig> = {};
|
||||
if (anchor === 'nw') {
|
||||
updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid);
|
||||
updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid);
|
||||
} else if (anchor === 'ne') {
|
||||
updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid);
|
||||
updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid);
|
||||
} else if (anchor === 'sw') {
|
||||
updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid);
|
||||
updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid);
|
||||
} else if (anchor === 'se') {
|
||||
updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid);
|
||||
updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid);
|
||||
}
|
||||
updateLayerConfig(layerProp, updates);
|
||||
};
|
||||
|
||||
const resizeLayerEdge = (layerProp: string, edge: 'n' | 's' | 'e' | 'w', delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||
const layer = getLayerConfig(layerProp);
|
||||
if (!layer) return;
|
||||
|
||||
const updates: Partial<LayerConfig> = {};
|
||||
if (edge === 'n') updates.y1 = Math.min(startGrid.y2, Math.max(1, startGrid.y1 + delta));
|
||||
if (edge === 's') updates.y2 = Math.max(startGrid.y1, Math.min(state.gridH, startGrid.y2 + delta));
|
||||
if (edge === 'w') updates.x1 = Math.min(startGrid.x2, Math.max(1, startGrid.x1 + delta));
|
||||
if (edge === 'e') updates.x2 = Math.max(startGrid.x1, Math.min(state.gridW, startGrid.x2 + delta));
|
||||
updateLayerConfig(layerProp, updates);
|
||||
};
|
||||
|
||||
const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '', backLayersStr: string = '') => {
|
||||
if (!path) {
|
||||
setState({ error: '未指定 CSV 文件路径' });
|
||||
|
|
@ -319,7 +355,6 @@ export function createDeckStore(
|
|||
`grid="${state.gridW}x${state.gridH}" `
|
||||
];
|
||||
|
||||
// 仅在非默认值时添加 bleed 和 padding
|
||||
if (state.bleed !== DECK_DEFAULTS.BLEED) {
|
||||
parts.push(`bleed="${state.bleed}" `);
|
||||
}
|
||||
|
|
@ -396,13 +431,16 @@ export function createDeckStore(
|
|||
updateBackLayerConfig,
|
||||
toggleBackLayerVisible,
|
||||
setIsEditing,
|
||||
setEditingLayer,
|
||||
updateLayerPosition,
|
||||
setSelectedLayer,
|
||||
setActiveSide,
|
||||
setIsSelecting,
|
||||
setSelectStart,
|
||||
setSelectEnd,
|
||||
cancelSelection,
|
||||
setDraggingState,
|
||||
moveLayer,
|
||||
resizeLayerCorner,
|
||||
resizeLayerEdge,
|
||||
loadCardsFromPath,
|
||||
setError,
|
||||
clearError,
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import {CSV} from "../../utils/csv-loader";
|
|||
|
||||
/**
|
||||
* 解析 layers 字符串
|
||||
* 格式:body:1,7-5,8 title:1,1-4,1f6.6s
|
||||
* f[fontSize] 表示字体大小(可选),方向字母在最后(可选)
|
||||
* 格式:body:1,7-5,8 title:1,1-4,1f6.6sl
|
||||
* f[fontSize] 表示字体大小(可选),方向字母(可选),对齐字母 l/c/r(可选)
|
||||
*/
|
||||
export function parseLayers(layersStr: string): Layer[] {
|
||||
if (!layersStr) return [];
|
||||
|
||||
const layers: Layer[] = [];
|
||||
// 匹配:prop:x1,y1-x2,y2[ffontSize][direction]
|
||||
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)(?:f([\d.]+))?([nsew])?/g;
|
||||
// 匹配:prop:x1,y1-x2,y2[ffontSize][direction][align]
|
||||
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)(?:f([\d.]+))?([nsew])?([lcr])?/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(layersStr)) !== null) {
|
||||
|
|
@ -22,7 +22,8 @@ export function parseLayers(layersStr: string): Layer[] {
|
|||
x2: parseInt(match[4]),
|
||||
y2: parseInt(match[5]),
|
||||
fontSize: match[6] ? parseFloat(match[6]) : undefined,
|
||||
orientation: match[7] as 'n' | 's' | 'e' | 'w' | undefined
|
||||
orientation: match[7] as 'n' | 's' | 'e' | 'w' | undefined,
|
||||
align: match[8] as 'l' | 'c' | 'r' | undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +44,9 @@ export function formatLayers(layers: LayerConfig[]): string {
|
|||
if (l.orientation && l.orientation !== 'n') {
|
||||
str += l.orientation;
|
||||
}
|
||||
if (l.align) {
|
||||
str += l.align;
|
||||
}
|
||||
return str;
|
||||
})
|
||||
.join(' ');
|
||||
|
|
@ -68,7 +72,8 @@ export function initLayerConfigsForSide(
|
|||
x2: existing?.x2 || 2,
|
||||
y2: existing?.y2 || 2,
|
||||
orientation: existing?.orientation,
|
||||
fontSize: existing?.fontSize
|
||||
fontSize: existing?.fontSize,
|
||||
align: existing?.align
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
import type { DeckStore } from './deckStore';
|
||||
|
||||
/**
|
||||
* 框选相关的操作(已整合到 deckStore)
|
||||
* 此 hook 用于处理卡牌预览区域的鼠标交互
|
||||
*/
|
||||
export function useCardSelection(store: DeckStore) {
|
||||
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement, dimensions: DeckStore['state']['dimensions']) => {
|
||||
if (!dimensions) return { gridX: 1, gridY: 1 };
|
||||
|
||||
const rect = cardEl.getBoundingClientRect();
|
||||
|
||||
const offsetX = (e.clientX - rect.left) / rect.width * dimensions.cardWidth;
|
||||
const offsetY = (e.clientY - rect.top) / rect.height * dimensions.cardHeight;
|
||||
|
||||
const gridX = Math.max(1, Math.floor((offsetX - dimensions.gridOriginX) / dimensions.cellWidth) + 1);
|
||||
const gridY = Math.max(1, Math.floor((offsetY - dimensions.gridOriginY) / dimensions.cellHeight) + 1);
|
||||
|
||||
return {
|
||||
gridX: Math.max(1, Math.min(dimensions.gridW, gridX)),
|
||||
gridY: Math.max(1, Math.min(dimensions.gridH, gridY))
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||
if (!store.state.isEditing || !store.state.editingLayer) return;
|
||||
|
||||
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
|
||||
|
||||
store.actions.setSelectStart({ x: gridX, y: gridY });
|
||||
store.actions.setSelectEnd({ x: gridX, y: gridY });
|
||||
store.actions.setIsSelecting(true);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||
if (!store.state.isSelecting) return;
|
||||
|
||||
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
|
||||
|
||||
store.actions.setSelectEnd({ x: gridX, y: gridY });
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!store.state.isSelecting || !store.state.editingLayer) return;
|
||||
|
||||
const start = store.state.selectStart!;
|
||||
const end = store.state.selectEnd!;
|
||||
|
||||
const x1 = Math.min(start.x, end.x);
|
||||
const y1 = Math.min(start.y, end.y);
|
||||
const x2 = Math.max(start.x, end.x);
|
||||
const y2 = Math.max(start.y, end.y);
|
||||
|
||||
store.actions.updateLayerPosition(x1, y1, x2, y2);
|
||||
store.actions.cancelSelection();
|
||||
};
|
||||
|
||||
return {
|
||||
onMouseDown: handleMouseDown,
|
||||
onMouseMove: handleMouseMove,
|
||||
onMouseUp: handleMouseUp,
|
||||
onMouseLeave: handleMouseUp
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算框选区域的样式
|
||||
*/
|
||||
export function getSelectionBoxStyle(
|
||||
selectStart: { x: number; y: number } | null,
|
||||
selectEnd: { x: number; y: number } | null,
|
||||
dims: { gridOriginX: number; gridOriginY: number; cellWidth: number; cellHeight: number } | null
|
||||
): { left: string; top: string; width: string; height: string } | null {
|
||||
if (!selectStart || !selectEnd || !dims) return null;
|
||||
|
||||
const x1 = Math.min(selectStart.x, selectEnd.x);
|
||||
const y1 = Math.min(selectStart.y, selectEnd.y);
|
||||
const x2 = Math.max(selectStart.x, selectEnd.x);
|
||||
const y2 = Math.max(selectStart.y, selectEnd.y);
|
||||
|
||||
const left = dims.gridOriginX + (x1 - 1) * dims.cellWidth;
|
||||
const top = dims.gridOriginY + (y1 - 1) * dims.cellHeight;
|
||||
const width = (x2 - x1 + 1) * dims.cellWidth;
|
||||
const height = (y2 - y1 + 1) * dims.cellHeight;
|
||||
|
||||
return {
|
||||
left: `${left}mm`,
|
||||
top: `${top}mm`,
|
||||
width: `${width}mm`,
|
||||
height: `${height}mm`
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import { createMemo } from 'solid-js';
|
||||
import type { DeckStore, DraggingState } from './deckStore';
|
||||
import type { LayerConfig } from '../types';
|
||||
|
||||
export interface LayerClickInfo {
|
||||
layerProp: string;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
}
|
||||
|
||||
export interface LayerInteractionHandlers {
|
||||
onLayerClick: (layerProp: string, e: MouseEvent) => void;
|
||||
onFrameMouseDown: (action: 'drag' | 'resize-corner' | 'resize-edge', anchor?: 'nw' | 'ne' | 'sw' | 'se', edge?: 'n' | 's' | 'e' | 'w', e?: MouseEvent) => void;
|
||||
onCardMouseMove: (e: MouseEvent, cardEl: HTMLElement) => void;
|
||||
onCardMouseUp: () => void;
|
||||
onCardClick: (e: MouseEvent, cardEl: HTMLElement) => void;
|
||||
getOverlappingLayers: (gridX: number, gridY: number) => LayerConfig[];
|
||||
}
|
||||
|
||||
export function useLayerInteraction(store: DeckStore): LayerInteractionHandlers {
|
||||
let clickStack: { gridX: number; gridY: number; layers: string[]; currentIndex: number } | null = null;
|
||||
|
||||
const currentLayerConfigs = () =>
|
||||
store.state.activeSide === 'front'
|
||||
? store.state.frontLayerConfigs.filter(l => l.visible)
|
||||
: store.state.backLayerConfigs.filter(l => l.visible);
|
||||
|
||||
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||
const dims = store.state.dimensions;
|
||||
if (!dims) return { gridX: 1, gridY: 1 };
|
||||
|
||||
const rect = cardEl.getBoundingClientRect();
|
||||
const mmToPxRatio = rect.width / dims.cardWidth;
|
||||
|
||||
const offsetX = (e.clientX - rect.left) / mmToPxRatio;
|
||||
const offsetY = (e.clientY - rect.top) / mmToPxRatio;
|
||||
|
||||
const gridX = Math.floor((offsetX - dims.gridOriginX) / dims.cellWidth) + 1;
|
||||
const gridY = Math.floor((offsetY - dims.gridOriginY) / dims.cellHeight) + 1;
|
||||
|
||||
return {
|
||||
gridX: Math.max(1, Math.min(dims.gridW, gridX)),
|
||||
gridY: Math.max(1, Math.min(dims.gridH, gridY))
|
||||
};
|
||||
};
|
||||
|
||||
const isPointInLayer = (gridX: number, gridY: number, layer: LayerConfig): boolean => {
|
||||
return gridX >= layer.x1 && gridX <= layer.x2 && gridY >= layer.y1 && gridY <= layer.y2;
|
||||
};
|
||||
|
||||
const getOverlappingLayers = (gridX: number, gridY: number): LayerConfig[] => {
|
||||
return currentLayerConfigs().filter(layer => isPointInLayer(gridX, gridY, layer));
|
||||
};
|
||||
|
||||
const handleLayerClick = (layerProp: string, e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const currentlySelected = store.state.selectedLayer;
|
||||
|
||||
if (currentlySelected === layerProp) {
|
||||
store.actions.setSelectedLayer(null);
|
||||
} else {
|
||||
store.actions.setSelectedLayer(layerProp);
|
||||
}
|
||||
|
||||
clickStack = null;
|
||||
};
|
||||
|
||||
const handleCardClick = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||
if (store.state.draggingState) return;
|
||||
|
||||
const { gridX, gridY } = calculateGridCoords(e, cardEl);
|
||||
const overlapping = getOverlappingLayers(gridX, gridY);
|
||||
|
||||
if (overlapping.length === 0) {
|
||||
store.actions.setSelectedLayer(null);
|
||||
clickStack = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const overlappingProps = overlapping.map(l => l.prop);
|
||||
|
||||
if (clickStack && clickStack.gridX === gridX && clickStack.gridY === gridY) {
|
||||
clickStack.currentIndex = (clickStack.currentIndex + 1) % overlappingProps.length;
|
||||
const nextLayer = overlappingProps[clickStack.currentIndex];
|
||||
store.actions.setSelectedLayer(nextLayer);
|
||||
} else {
|
||||
clickStack = {
|
||||
gridX,
|
||||
gridY,
|
||||
layers: overlappingProps,
|
||||
currentIndex: 0
|
||||
};
|
||||
store.actions.setSelectedLayer(overlappingProps[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrameMouseDown = (
|
||||
action: 'drag' | 'resize-corner' | 'resize-edge',
|
||||
anchor?: 'nw' | 'ne' | 'sw' | 'se',
|
||||
edge?: 'n' | 's' | 'e' | 'w',
|
||||
e?: MouseEvent
|
||||
) => {
|
||||
if (!store.state.selectedLayer) return;
|
||||
if (e) e.stopPropagation();
|
||||
|
||||
const layer = currentLayerConfigs().find(l => l.prop === store.state.selectedLayer);
|
||||
if (!layer) return;
|
||||
|
||||
const startX = e?.clientX ?? 0;
|
||||
const startY = e?.clientY ?? 0;
|
||||
|
||||
const draggingState: DraggingState = {
|
||||
layer: store.state.selectedLayer,
|
||||
action,
|
||||
anchor,
|
||||
edge,
|
||||
startX,
|
||||
startY,
|
||||
startGrid: {
|
||||
x1: layer.x1,
|
||||
y1: layer.y1,
|
||||
x2: layer.x2,
|
||||
y2: layer.y2
|
||||
}
|
||||
};
|
||||
|
||||
store.actions.setDraggingState(draggingState);
|
||||
};
|
||||
|
||||
const handleCardMouseMove = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||
const dragging = store.state.draggingState;
|
||||
if (!dragging) return;
|
||||
|
||||
const dims = store.state.dimensions;
|
||||
if (!dims) return;
|
||||
|
||||
const rect = cardEl.getBoundingClientRect();
|
||||
const mmToPxRatio = rect.width / dims.cardWidth;
|
||||
|
||||
const deltaX = e.clientX - dragging.startX;
|
||||
const deltaY = e.clientY - dragging.startY;
|
||||
|
||||
const deltaMmX = deltaX / mmToPxRatio;
|
||||
const deltaMmY = deltaY / mmToPxRatio;
|
||||
|
||||
const deltaGridX = deltaMmX / dims.cellWidth;
|
||||
const deltaGridY = deltaMmY / dims.cellHeight;
|
||||
|
||||
const startGrid = dragging.startGrid;
|
||||
|
||||
if (dragging.action === 'drag') {
|
||||
store.actions.moveLayer(dragging.layer, Math.round(deltaGridX), Math.round(deltaGridY), startGrid);
|
||||
} else if (dragging.action === 'resize-corner' && dragging.anchor) {
|
||||
store.actions.resizeLayerCorner(dragging.layer, dragging.anchor, Math.round(deltaGridX), Math.round(deltaGridY), startGrid);
|
||||
} else if (dragging.action === 'resize-edge' && dragging.edge) {
|
||||
const delta = dragging.edge === 'n' || dragging.edge === 's' ? Math.round(deltaGridY) : Math.round(deltaGridX);
|
||||
store.actions.resizeLayerEdge(dragging.layer, dragging.edge, delta, startGrid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCardMouseUp = () => {
|
||||
if (store.state.draggingState) {
|
||||
const layer = store.state.draggingState.layer;
|
||||
const dragging = store.state.draggingState;
|
||||
|
||||
store.actions.setDraggingState(null);
|
||||
|
||||
const newLayerConfig = currentLayerConfigs().find(l => l.prop === layer);
|
||||
if (newLayerConfig && dragging.startGrid) {
|
||||
store.actions.setDraggingState({
|
||||
...dragging,
|
||||
startGrid: {
|
||||
x1: newLayerConfig.x1,
|
||||
y1: newLayerConfig.y1,
|
||||
x2: newLayerConfig.x2,
|
||||
y2: newLayerConfig.y2
|
||||
}
|
||||
});
|
||||
store.actions.setDraggingState(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onLayerClick: handleLayerClick,
|
||||
onFrameMouseDown: handleFrameMouseDown,
|
||||
onCardMouseMove: handleCardMouseMove,
|
||||
onCardMouseUp: handleCardMouseUp,
|
||||
onCardClick: handleCardClick,
|
||||
getOverlappingLayers
|
||||
};
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ export interface Layer {
|
|||
y2: number;
|
||||
orientation?: 'n' | 's' | 'e' | 'w';
|
||||
fontSize?: number;
|
||||
align?: 'l' | 'c' | 'r';
|
||||
}
|
||||
|
||||
export interface LayerConfig {
|
||||
|
|
@ -25,6 +26,7 @@ export interface LayerConfig {
|
|||
y2: number;
|
||||
orientation?: 'n' | 's' | 'e' | 'w';
|
||||
fontSize?: number;
|
||||
align?: 'l' | 'c' | 'r';
|
||||
}
|
||||
|
||||
export interface Dimensions {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ export function parseCSVString<T = Record<string, string>>(csvString: string, so
|
|||
columns: true,
|
||||
comment: '#',
|
||||
trim: true,
|
||||
skipEmptyLines: true
|
||||
skipEmptyLines: true,
|
||||
bom: true
|
||||
});
|
||||
|
||||
const result = records as Record<string, string>[];
|
||||
|
|
@ -112,28 +113,7 @@ export async function loadCSV<T = Record<string, string>>(pathOrContent: string)
|
|||
|
||||
// 从索引获取文件内容
|
||||
const content = await getIndexedData(pathOrContent);
|
||||
|
||||
// 解析 front matter
|
||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||
|
||||
const records = parse(remainingContent, {
|
||||
columns: true,
|
||||
comment: '#',
|
||||
trim: true,
|
||||
skipEmptyLines: true
|
||||
});
|
||||
|
||||
const result = records as Record<string, string>[];
|
||||
// 添加 front matter 到结果中
|
||||
const csvResult = result as CSV<T>;
|
||||
if (frontmatter) {
|
||||
csvResult.frontmatter = frontmatter;
|
||||
for(const each of result){
|
||||
Object.assign(each, frontmatter);
|
||||
}
|
||||
}
|
||||
csvResult.sourcePath = pathOrContent;
|
||||
return csvResult;
|
||||
return parseCSVString<T>(content, pathOrContent);
|
||||
}
|
||||
|
||||
type JSONData = JSONArray | JSONObject | string | number | boolean | null;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
/* icon */
|
||||
icon, pull{
|
||||
@apply inline-block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
width: 1.5em;
|
||||
height: 1.28em;
|
||||
vertical-align: text-bottom;
|
||||
--icon-src: '';
|
||||
background-image: var(--icon-src);
|
||||
|
|
@ -13,10 +13,9 @@ icon, pull{
|
|||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
icon.mini{
|
||||
width: 1.5em;
|
||||
height: 1.28em;
|
||||
vertical-align: text-bottom;
|
||||
icon.big{
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
pull{
|
||||
margin-right: -.5em;
|
||||
|
|
|
|||
Loading…
Reference in New Issue