fix: state tracking

This commit is contained in:
hypercross 2026-02-27 14:58:44 +08:00
parent 0aaadea2da
commit 8ddc2a672a
5 changed files with 84 additions and 80 deletions

View File

@ -1,8 +1,8 @@
import { Show, For } from 'solid-js'; import { Show, For } from 'solid-js';
import { marked } from '../markdown'; import { marked } from '../markdown';
import { getLayerStyle } from './utils/dimensions'; import { getLayerStyle } from './utils/dimensions';
import {getSelectionBoxStyle, useSelection} from './stores/use-selection'; import { getSelectionBoxStyle, useSelection } from './stores/use-selection';
import {DeckStore} from "./stores/deckStore"; import type { DeckStore } from "./stores/deckStore";
export interface CardPreviewProps { export interface CardPreviewProps {
store: DeckStore; store: DeckStore;
@ -11,30 +11,30 @@ export interface CardPreviewProps {
/** /**
* layer * layer
*/ */
function renderLayer(layer: { prop: string }, cardData: DeckStore['cards'][number]): string { function renderLayer(layer: { prop: string }, cardData: DeckStore['state']['cards'][number]): string {
const content = cardData[layer.prop] || ''; const content = cardData[layer.prop] || '';
return marked.parse(content) as string; return marked.parse(content) as string;
} }
export function CardPreview(props: CardPreviewProps) { export function CardPreview(props: CardPreviewProps) {
const currentCard = () => props.store.cards[props.store.activeTab]; const currentCard = () => props.store.state.cards[props.store.state.activeTab];
const visibleLayers = () => props.store.layerConfigs.filter((l) => l.visible); const visibleLayers = () => props.store.state.layerConfigs.filter((l) => l.visible);
const selectionStyle = () => const selectionStyle = () =>
getSelectionBoxStyle(props.store.selectStart, props.store.selectEnd, props.store.dimensions); getSelectionBoxStyle(props.store.state.selectStart, props.store.state.selectEnd, props.store.state.dimensions);
const selection = useSelection(props.store); const selection = useSelection(props.store);
let cardRef: HTMLDivElement | undefined; let cardRef: HTMLDivElement | undefined;
return ( return (
<div class="flex justify-center"> <div class="flex justify-center">
<Show when={props.store.activeTab < props.store.cards.length}> <Show when={props.store.state.activeTab < props.store.state.cards.length}>
<div <div
ref={cardRef} ref={cardRef}
class="relative bg-white border border-gray-300 shadow-lg" class="relative bg-white border border-gray-300 shadow-lg"
style={{ style={{
width: `${props.store.dimensions?.cardWidth}mm`, width: `${props.store.state.dimensions?.cardWidth}mm`,
height: `${props.store.dimensions?.cardHeight}mm` height: `${props.store.state.dimensions?.cardHeight}mm`
}} }}
onMouseDown={(e) => selection.onMouseDown(e, cardRef!)} onMouseDown={(e) => selection.onMouseDown(e, cardRef!)}
onMouseMove={(e) => selection.onMouseMove(e, cardRef!)} onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
@ -42,7 +42,7 @@ export function CardPreview(props: CardPreviewProps) {
onMouseLeave={selection.onMouseLeave} onMouseLeave={selection.onMouseLeave}
> >
{/* 框选遮罩 */} {/* 框选遮罩 */}
<Show when={props.store.isSelecting && selectionStyle()}> <Show when={props.store.state.isSelecting && selectionStyle()}>
<div <div
class="absolute bg-blue-500/30 border-2 border-blue-500 pointer-events-none" class="absolute bg-blue-500/30 border-2 border-blue-500 pointer-events-none"
style={selectionStyle()!} style={selectionStyle()!}
@ -53,28 +53,28 @@ export function CardPreview(props: CardPreviewProps) {
<div <div
class="absolute" class="absolute"
style={{ style={{
left: `${props.store.dimensions?.gridOriginX}mm`, left: `${props.store.state.dimensions?.gridOriginX}mm`,
top: `${props.store.dimensions?.gridOriginY}mm`, top: `${props.store.state.dimensions?.gridOriginY}mm`,
width: `${props.store.dimensions?.gridAreaWidth}mm`, width: `${props.store.state.dimensions?.gridAreaWidth}mm`,
height: `${props.store.dimensions?.gridAreaHeight}mm` height: `${props.store.state.dimensions?.gridAreaHeight}mm`
}} }}
> >
{/* 编辑模式下的网格线 */} {/* 编辑模式下的网格线 */}
<Show when={props.store.isEditing && !props.store.fixed}> <Show when={props.store.state.isEditing && !props.store.state.fixed}>
<div class="absolute inset-0 pointer-events-none"> <div class="absolute inset-0 pointer-events-none">
<For each={Array.from({ length: (props.store.dimensions?.gridW || 0) - 1 })}> <For each={Array.from({ length: (props.store.state.dimensions?.gridW || 0) - 1 })}>
{(_, i) => ( {(_, i) => (
<div <div
class="absolute top-0 bottom-0 border-r border-dashed border-gray-300" class="absolute top-0 bottom-0 border-r border-dashed border-gray-300"
style={{ left: `${(i() + 1) * (props.store.dimensions?.cellWidth || 0)}mm` }} style={{ left: `${(i() + 1) * (props.store.state.dimensions?.cellWidth || 0)}mm` }}
/> />
)} )}
</For> </For>
<For each={Array.from({ length: (props.store.dimensions?.gridH || 0) - 1 })}> <For each={Array.from({ length: (props.store.state.dimensions?.gridH || 0) - 1 })}>
{(_, i) => ( {(_, i) => (
<div <div
class="absolute left-0 right-0 border-b border-dashed border-gray-300" class="absolute left-0 right-0 border-b border-dashed border-gray-300"
style={{ top: `${(i() + 1) * (props.store.dimensions?.cellHeight || 0)}mm` }} style={{ top: `${(i() + 1) * (props.store.state.dimensions?.cellHeight || 0)}mm` }}
/> />
)} )}
</For> </For>
@ -84,12 +84,12 @@ export function CardPreview(props: CardPreviewProps) {
{/* 渲染每个 layer */} {/* 渲染每个 layer */}
<For each={visibleLayers()}> <For each={visibleLayers()}>
{(layer) => { {(layer) => {
const style = getLayerStyle(layer, props.store.dimensions!); const style = getLayerStyle(layer, props.store.state.dimensions!);
return ( return (
<div <div
class={`absolute flex items-center justify-center text-center prose prose-sm ${ class={`absolute flex items-center justify-center text-center prose prose-sm ${
props.store.isEditing ? 'bg-blue-500/20 ring-2 ring-blue-500' : '' props.store.state.isEditing ? 'bg-blue-500/20 ring-2 ring-blue-500' : ''
}`} }`}
style={style} style={style}
innerHTML={renderLayer(layer, currentCard())} innerHTML={renderLayer(layer, currentCard())}

View File

@ -1,10 +1,10 @@
import { For } from 'solid-js'; import { For } from 'solid-js';
import { DeckStore } from './stores/deckStore'; import type { DeckStore } from './stores/deckStore';
export interface DataEditorPanelProps { export interface DataEditorPanelProps {
activeTab: number; activeTab: number;
cards: DeckStore['cards']; cards: DeckStore['state']['cards'];
updateCardData: DeckStore['updateCardData']; updateCardData: DeckStore['actions']['updateCardData'];
} }
export interface PropertiesEditorPanelProps { export interface PropertiesEditorPanelProps {
@ -42,7 +42,7 @@ export function DataEditorPanel(props: DataEditorPanelProps) {
*/ */
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) { export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
const { store } = props; const { store } = props;
return ( return (
<div class="w-64 flex-shrink-0"> <div class="w-64 flex-shrink-0">
<h3 class="font-bold mb-2"></h3> <h3 class="font-bold mb-2"></h3>
@ -53,8 +53,8 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
<input <input
type="text" type="text"
class="w-full border border-gray-300 rounded px-2 py-1 text-sm" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
value={store.size} value={store.state.size}
onInput={(e) => store.setSize(e.target.value)} onInput={(e) => store.actions.setSize(e.target.value)}
/> />
</div> </div>
@ -63,8 +63,8 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
<input <input
type="text" type="text"
class="w-full border border-gray-300 rounded px-2 py-1 text-sm" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
value={store.grid} value={store.state.grid}
onInput={(e) => store.setGrid(e.target.value)} onInput={(e) => store.actions.setGrid(e.target.value)}
/> />
</div> </div>
@ -73,8 +73,8 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
<input <input
type="text" type="text"
class="w-full border border-gray-300 rounded px-2 py-1 text-sm" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
value={store.bleed} value={store.state.bleed}
onInput={(e) => store.setBleed(e.target.value)} onInput={(e) => store.actions.setBleed(e.target.value)}
/> />
</div> </div>
@ -83,33 +83,33 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
<input <input
type="text" type="text"
class="w-full border border-gray-300 rounded px-2 py-1 text-sm" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
value={store.padding} value={store.state.padding}
onInput={(e) => store.setPadding(e.target.value)} onInput={(e) => store.actions.setPadding(e.target.value)}
/> />
</div> </div>
<hr class="my-4" /> <hr class="my-4" />
<h4 class="font-medium text-sm text-gray-700"></h4> <h4 class="font-medium text-sm text-gray-700"></h4>
<For each={store.layerConfigs}> <For each={store.state.layerConfigs}>
{(layer) => ( {(layer) => (
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={layer.visible} checked={layer.visible}
onChange={() => store.toggleLayerVisible(layer.prop)} onChange={() => store.actions.toggleLayerVisible(layer.prop)}
class="cursor-pointer" class="cursor-pointer"
/> />
<span class="text-sm flex-1">{layer.prop}</span> <span class="text-sm flex-1">{layer.prop}</span>
<button <button
onClick={() => store.setEditingLayer(layer.prop)} onClick={() => store.actions.setEditingLayer(layer.prop)}
class={`text-xs px-2 py-0.5 rounded cursor-pointer ${ class={`text-xs px-2 py-0.5 rounded cursor-pointer ${
store.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'
}`} }`}
> >
{store.editingLayer === layer.prop ? '✓ 框选' : '编辑位置'} {store.state.editingLayer === layer.prop ? '✓ 框选' : '编辑位置'}
</button> </button>
</div> </div>
)} )}
@ -118,7 +118,7 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
<hr class="my-4" /> <hr class="my-4" />
<button <button
onClick={store.copyCode} onClick={store.actions.copyCode}
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium cursor-pointer" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium cursor-pointer"
> >
📋 📋

View File

@ -45,10 +45,10 @@ customElement<DeckProps>('md-deck', {
const resolvedSrc = resolvePath(articlePath, csvPath); const resolvedSrc = resolvePath(articlePath, csvPath);
// 初始化 store 属性 // 初始化 store 属性
store.setSize(props.size || '54x86'); store.actions.setSize(props.size || '54x86');
store.setGrid(props.grid || '5x8'); store.actions.setGrid(props.grid || '5x8');
store.setBleed(props.bleed || '1'); store.actions.setBleed(props.bleed || '1');
store.setPadding(props.padding || '2'); store.actions.setPadding(props.padding || '2');
// 加载 CSV 文件 // 加载 CSV 文件
const [csvData, { refetch }] = createResource(() => resolvedSrc, loadCSV); const [csvData, { refetch }] = createResource(() => resolvedSrc, loadCSV);
@ -60,13 +60,13 @@ customElement<DeckProps>('md-deck', {
const error = csvData.error; const error = csvData.error;
if (error) { if (error) {
store.setError(`加载 CSV 失败:${error.message}`); store.actions.setError(`加载 CSV 失败:${error.message}`);
return; return;
} }
if (!loading && data) { if (!loading && data) {
store.loadCards(data); store.actions.loadCards(data);
store.setLayerConfigs(initLayerConfigs(data, (props.layers as string) || '')); store.actions.setLayerConfigs(initLayerConfigs(data, (props.layers as string) || ''));
} }
}); });
@ -78,11 +78,11 @@ customElement<DeckProps>('md-deck', {
return ( return (
<div class="md-deck flex gap-4"> <div class="md-deck flex gap-4">
{/* 左侧CSV 数据编辑 */} {/* 左侧CSV 数据编辑 */}
<Show when={store.isEditing && !store.fixed}> <Show when={store.state.isEditing && !store.state.fixed}>
<DataEditorPanel <DataEditorPanel
activeTab={store.activeTab} activeTab={store.state.activeTab}
cards={store.cards} cards={store.state.cards}
updateCardData={store.updateCardData} updateCardData={store.actions.updateCardData}
/> />
</Show> </Show>
@ -91,22 +91,22 @@ customElement<DeckProps>('md-deck', {
{/* Tab 选择器 */} {/* Tab 选择器 */}
<div class="flex items-center gap-2 border-b border-gray-200 pb-2 mb-4"> <div class="flex items-center gap-2 border-b border-gray-200 pb-2 mb-4">
<button <button
onClick={() => store.setIsEditing(!store.isEditing)} onClick={() => store.actions.setIsEditing(!store.state.isEditing)}
class={`px-3 py-1 rounded text-sm font-medium transition-colors ${ class={`px-3 py-1 rounded text-sm font-medium transition-colors ${
store.isEditing && !store.fixed store.state.isEditing && !store.state.fixed
? 'bg-blue-100 text-blue-600' ? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} cursor-pointer`} } cursor-pointer`}
> >
{store.isEditing ? '✓ 编辑中' : '✏️ 编辑'} {store.state.isEditing ? '✓ 编辑中' : '✏️ 编辑'}
</button> </button>
<div class="flex gap-1 overflow-x-auto flex-1 min-w-0 flex-wrap"> <div class="flex gap-1 overflow-x-auto flex-1 min-w-0 flex-wrap">
<For each={store.cards}> <For each={store.state.cards}>
{(card, index) => ( {(card, index) => (
<button <button
onClick={() => store.setActiveTab(index())} onClick={() => store.actions.setActiveTab(index())}
class={`font-medium transition-colors flex-shrink-0 min-w-[1.6em] cursor-pointer px-2 py-1 rounded ${ class={`font-medium transition-colors flex-shrink-0 min-w-[1.6em] cursor-pointer px-2 py-1 rounded ${
store.activeTab === index() store.state.activeTab === index()
? 'bg-blue-100 text-blue-600 border-b-2 border-blue-600' ? 'bg-blue-100 text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`} }`}
@ -119,9 +119,9 @@ customElement<DeckProps>('md-deck', {
</div> </div>
{/* 错误提示 */} {/* 错误提示 */}
<Show when={store.error}> <Show when={store.state.error}>
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4"> <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
{store.error} {store.state.error}
</div> </div>
</Show> </Show>
@ -133,12 +133,12 @@ customElement<DeckProps>('md-deck', {
</Show> </Show>
{/* 卡牌预览 */} {/* 卡牌预览 */}
<Show when={!csvData.loading && store.cards.length > 0 && !store.error}> <Show when={!csvData.loading && store.state.cards.length > 0 && !store.state.error}>
<CardPreview store={store} /> <CardPreview store={store} />
</Show> </Show>
{/* 空状态 */} {/* 空状态 */}
<Show when={!csvData.loading && store.cards.length === 0 && !store.error}> <Show when={!csvData.loading && store.state.cards.length === 0 && !store.state.error}>
<div class="text-center text-gray-500 py-8"> <div class="text-center text-gray-500 py-8">
</div> </div>
@ -146,7 +146,7 @@ customElement<DeckProps>('md-deck', {
</div> </div>
{/* 右侧:属性编辑表单 */} {/* 右侧:属性编辑表单 */}
<Show when={store.isEditing && !store.fixed}> <Show when={store.state.isEditing && !store.state.fixed}>
<PropertiesEditorPanel store={store} /> <PropertiesEditorPanel store={store} />
</Show> </Show>
</div> </div>

View File

@ -71,7 +71,10 @@ export interface DeckActions {
copyCode: () => void; copyCode: () => void;
} }
export interface DeckStore extends DeckState, DeckActions {} export interface DeckStore {
state: DeckState;
actions: DeckActions;
}
/** /**
* deck store * deck store
@ -187,8 +190,7 @@ export function createDeckStore(): DeckStore {
}); });
}; };
return { const actions: DeckActions = {
...state,
setSize, setSize,
setGrid, setGrid,
setBleed, setBleed,
@ -211,4 +213,6 @@ export function createDeckStore(): DeckStore {
generateCode, generateCode,
copyCode copyCode
}; };
return { state, actions };
} }

View File

@ -5,9 +5,9 @@ import type { DeckStore } from './deckStore';
* hook * hook
*/ */
export function useSelection(store: DeckStore) { export function useSelection(store: DeckStore) {
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement, dimensions: DeckStore['dimensions']) => { const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement, dimensions: DeckStore['state']['dimensions']) => {
if (!dimensions) return { gridX: 1, gridY: 1 }; if (!dimensions) return { gridX: 1, gridY: 1 };
const rect = cardEl.getBoundingClientRect(); const rect = cardEl.getBoundingClientRect();
const offsetX = (e.clientX - rect.left) / rect.width * dimensions.cardWidth; const offsetX = (e.clientX - rect.left) / rect.width * dimensions.cardWidth;
@ -23,36 +23,36 @@ export function useSelection(store: DeckStore) {
}; };
const handleMouseDown = (e: MouseEvent, cardEl: HTMLElement) => { const handleMouseDown = (e: MouseEvent, cardEl: HTMLElement) => {
if (!store.isEditing || !store.editingLayer) return; if (!store.state.isEditing || !store.state.editingLayer) return;
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.dimensions); const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
store.setSelectStart({ x: gridX, y: gridY }); store.actions.setSelectStart({ x: gridX, y: gridY });
store.setSelectEnd({ x: gridX, y: gridY }); store.actions.setSelectEnd({ x: gridX, y: gridY });
store.setIsSelecting(true); store.actions.setIsSelecting(true);
}; };
const handleMouseMove = (e: MouseEvent, cardEl: HTMLElement) => { const handleMouseMove = (e: MouseEvent, cardEl: HTMLElement) => {
if (!store.isSelecting) return; if (!store.state.isSelecting) return;
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.dimensions); const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
store.setSelectEnd({ x: gridX, y: gridY }); store.actions.setSelectEnd({ x: gridX, y: gridY });
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (!store.isSelecting || !store.editingLayer) return; if (!store.state.isSelecting || !store.state.editingLayer) return;
const start = store.selectStart!; const start = store.state.selectStart!;
const end = store.selectEnd!; const end = store.state.selectEnd!;
const x1 = Math.min(start.x, end.x); const x1 = Math.min(start.x, end.x);
const y1 = Math.min(start.y, end.y); const y1 = Math.min(start.y, end.y);
const x2 = Math.max(start.x, end.x); const x2 = Math.max(start.x, end.x);
const y2 = Math.max(start.y, end.y); const y2 = Math.max(start.y, end.y);
store.updateLayerPosition(x1, y1, x2, y2); store.actions.updateLayerPosition(x1, y1, x2, y2);
store.cancelSelection(); store.actions.cancelSelection();
}; };
return { return {