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 { marked } from '../markdown';
import { getLayerStyle } from './utils/dimensions';
import {getSelectionBoxStyle, useSelection} from './stores/use-selection';
import {DeckStore} from "./stores/deckStore";
import { getSelectionBoxStyle, useSelection } from './stores/use-selection';
import type { DeckStore } from "./stores/deckStore";
export interface CardPreviewProps {
store: DeckStore;
@ -11,30 +11,30 @@ export interface CardPreviewProps {
/**
* 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] || '';
return marked.parse(content) as string;
}
export function CardPreview(props: CardPreviewProps) {
const currentCard = () => props.store.cards[props.store.activeTab];
const visibleLayers = () => props.store.layerConfigs.filter((l) => l.visible);
const currentCard = () => props.store.state.cards[props.store.state.activeTab];
const visibleLayers = () => props.store.state.layerConfigs.filter((l) => l.visible);
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);
let cardRef: HTMLDivElement | undefined;
return (
<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
ref={cardRef}
class="relative bg-white border border-gray-300 shadow-lg"
style={{
width: `${props.store.dimensions?.cardWidth}mm`,
height: `${props.store.dimensions?.cardHeight}mm`
width: `${props.store.state.dimensions?.cardWidth}mm`,
height: `${props.store.state.dimensions?.cardHeight}mm`
}}
onMouseDown={(e) => selection.onMouseDown(e, cardRef!)}
onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
@ -42,7 +42,7 @@ export function CardPreview(props: CardPreviewProps) {
onMouseLeave={selection.onMouseLeave}
>
{/* 框选遮罩 */}
<Show when={props.store.isSelecting && selectionStyle()}>
<Show when={props.store.state.isSelecting && selectionStyle()}>
<div
class="absolute bg-blue-500/30 border-2 border-blue-500 pointer-events-none"
style={selectionStyle()!}
@ -53,28 +53,28 @@ export function CardPreview(props: CardPreviewProps) {
<div
class="absolute"
style={{
left: `${props.store.dimensions?.gridOriginX}mm`,
top: `${props.store.dimensions?.gridOriginY}mm`,
width: `${props.store.dimensions?.gridAreaWidth}mm`,
height: `${props.store.dimensions?.gridAreaHeight}mm`
left: `${props.store.state.dimensions?.gridOriginX}mm`,
top: `${props.store.state.dimensions?.gridOriginY}mm`,
width: `${props.store.state.dimensions?.gridAreaWidth}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">
<For each={Array.from({ length: (props.store.dimensions?.gridW || 0) - 1 })}>
<For each={Array.from({ length: (props.store.state.dimensions?.gridW || 0) - 1 })}>
{(_, i) => (
<div
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 each={Array.from({ length: (props.store.dimensions?.gridH || 0) - 1 })}>
<For each={Array.from({ length: (props.store.state.dimensions?.gridH || 0) - 1 })}>
{(_, i) => (
<div
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>
@ -84,12 +84,12 @@ export function CardPreview(props: CardPreviewProps) {
{/* 渲染每个 layer */}
<For each={visibleLayers()}>
{(layer) => {
const style = getLayerStyle(layer, props.store.dimensions!);
const style = getLayerStyle(layer, props.store.state.dimensions!);
return (
<div
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}
innerHTML={renderLayer(layer, currentCard())}

View File

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

View File

@ -45,10 +45,10 @@ customElement<DeckProps>('md-deck', {
const resolvedSrc = resolvePath(articlePath, csvPath);
// 初始化 store 属性
store.setSize(props.size || '54x86');
store.setGrid(props.grid || '5x8');
store.setBleed(props.bleed || '1');
store.setPadding(props.padding || '2');
store.actions.setSize(props.size || '54x86');
store.actions.setGrid(props.grid || '5x8');
store.actions.setBleed(props.bleed || '1');
store.actions.setPadding(props.padding || '2');
// 加载 CSV 文件
const [csvData, { refetch }] = createResource(() => resolvedSrc, loadCSV);
@ -60,13 +60,13 @@ customElement<DeckProps>('md-deck', {
const error = csvData.error;
if (error) {
store.setError(`加载 CSV 失败:${error.message}`);
store.actions.setError(`加载 CSV 失败:${error.message}`);
return;
}
if (!loading && data) {
store.loadCards(data);
store.setLayerConfigs(initLayerConfigs(data, (props.layers as string) || ''));
store.actions.loadCards(data);
store.actions.setLayerConfigs(initLayerConfigs(data, (props.layers as string) || ''));
}
});
@ -78,11 +78,11 @@ customElement<DeckProps>('md-deck', {
return (
<div class="md-deck flex gap-4">
{/* 左侧CSV 数据编辑 */}
<Show when={store.isEditing && !store.fixed}>
<Show when={store.state.isEditing && !store.state.fixed}>
<DataEditorPanel
activeTab={store.activeTab}
cards={store.cards}
updateCardData={store.updateCardData}
activeTab={store.state.activeTab}
cards={store.state.cards}
updateCardData={store.actions.updateCardData}
/>
</Show>
@ -91,22 +91,22 @@ customElement<DeckProps>('md-deck', {
{/* Tab 选择器 */}
<div class="flex items-center gap-2 border-b border-gray-200 pb-2 mb-4">
<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 ${
store.isEditing && !store.fixed
store.state.isEditing && !store.state.fixed
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} cursor-pointer`}
>
{store.isEditing ? '✓ 编辑中' : '✏️ 编辑'}
{store.state.isEditing ? '✓ 编辑中' : '✏️ 编辑'}
</button>
<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) => (
<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 ${
store.activeTab === index()
store.state.activeTab === index()
? 'bg-blue-100 text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
@ -119,9 +119,9 @@ customElement<DeckProps>('md-deck', {
</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">
{store.error}
{store.state.error}
</div>
</Show>
@ -133,12 +133,12 @@ customElement<DeckProps>('md-deck', {
</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} />
</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>
@ -146,7 +146,7 @@ customElement<DeckProps>('md-deck', {
</div>
{/* 右侧:属性编辑表单 */}
<Show when={store.isEditing && !store.fixed}>
<Show when={store.state.isEditing && !store.state.fixed}>
<PropertiesEditorPanel store={store} />
</Show>
</div>

View File

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

View File

@ -5,9 +5,9 @@ import type { DeckStore } from './deckStore';
* hook
*/
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 };
const rect = cardEl.getBoundingClientRect();
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) => {
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.setSelectEnd({ x: gridX, y: gridY });
store.setIsSelecting(true);
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.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 = () => {
if (!store.isSelecting || !store.editingLayer) return;
if (!store.state.isSelecting || !store.state.editingLayer) return;
const start = store.selectStart!;
const end = store.selectEnd!;
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.updateLayerPosition(x1, y1, x2, y2);
store.cancelSelection();
store.actions.updateLayerPosition(x1, y1, x2, y2);
store.actions.cancelSelection();
};
return {