diff --git a/src/components/card-preview.tsx b/src/components/card-preview.tsx
deleted file mode 100644
index ce7dcc4..0000000
--- a/src/components/card-preview.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Show, For } from 'solid-js';
-import { marked } from '../markdown';
-import { getLayerStyle } from './utils/dimensions';
-import { getSelectionBoxStyle, useSelection } from './stores/use-selection';
-import type { DeckStore } from "./stores/deckStore";
-
-export interface CardPreviewProps {
- store: DeckStore;
-}
-
-/**
- * 渲染 layer 内容
- */
-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.state.cards[props.store.state.activeTab];
- const visibleLayers = () => props.store.state.layerConfigs.filter((l) => l.visible);
- const selectionStyle = () =>
- getSelectionBoxStyle(props.store.state.selectStart, props.store.state.selectEnd, props.store.state.dimensions);
-
- const selection = useSelection(props.store);
-
- let cardRef: HTMLDivElement | undefined;
-
- return (
-
-
- selection.onMouseDown(e, cardRef!)}
- onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
- onMouseUp={selection.onMouseUp}
- onMouseLeave={selection.onMouseLeave}
- >
- {/* 框选遮罩 */}
-
-
-
-
- {/* 网格区域容器 */}
-
- {/* 编辑模式下的网格线 */}
-
-
-
- {(_, i) => (
-
- )}
-
-
- {(_, i) => (
-
- )}
-
-
-
-
- {/* 渲染每个 layer */}
-
- {(layer) => {
- const style = getLayerStyle(layer, props.store.state.dimensions!);
-
- return (
-
- );
- }}
-
-
-
-
-
- );
-}
diff --git a/src/components/md-deck.tsx b/src/components/md-deck.tsx
deleted file mode 100644
index 8ba99b6..0000000
--- a/src/components/md-deck.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { customElement, noShadowDOM } from 'solid-element';
-import { Show, createEffect, createResource, For, onCleanup } from 'solid-js';
-import { resolvePath } from './utils/path';
-import { loadCSV } from './utils/csv-loader';
-import { initLayerConfigs } from './utils/layer-parser';
-import { createDeckStore } from './stores/deckStore';
-import { CardPreview } from './card-preview';
-import { DataEditorPanel, PropertiesEditorPanel } from './editor-panel';
-
-interface DeckProps {
- size?: string;
- grid?: string;
- bleed?: string;
- padding?: string;
- layers?: string;
- fixed?: boolean | string;
-}
-
-customElement('md-deck', {
- size: '54x86',
- grid: '5x8',
- bleed: '1',
- padding: '2',
- layers: '',
- fixed: false
-}, (props, { element }) => {
- noShadowDOM();
-
- // 创建统一的 store
- const store = createDeckStore();
-
- // 从 element 的 textContent 获取 CSV 路径
- const csvPath = element?.textContent?.trim() || '';
-
- // 隐藏原始文本内容
- if (element) {
- element.textContent = '';
- }
-
- // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
- const articleEl = element?.closest('article[data-src]');
- const articlePath = articleEl?.getAttribute('data-src') || '';
-
- // 解析相对路径
- const resolvedSrc = resolvePath(articlePath, csvPath);
-
- // 初始化 store 属性
- 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);
-
- // 处理 CSV 数据加载结果
- createEffect(() => {
- const data = csvData();
- const loading = csvData.loading;
- const error = csvData.error;
-
- if (error) {
- store.actions.setError(`加载 CSV 失败:${error.message}`);
- return;
- }
-
- if (!loading && data) {
- store.actions.loadCards(data);
- store.actions.setLayerConfigs(initLayerConfigs(data, (props.layers as string) || ''));
- }
- });
-
- // 清理函数
- onCleanup(() => {
- // 可以在这里清理资源
- });
-
- return (
-
- {/* 左侧:CSV 数据编辑 */}
-
-
-
-
- {/* 中间:卡牌预览和控制 */}
-
- {/* Tab 选择器 */}
-
-
-
-
- {(card, index) => (
-
- )}
-
-
-
-
- {/* 错误提示 */}
-
-
- {store.state.error}
-
-
-
- {/* 加载状态 */}
-
-
- 加载卡牌数据中...
-
-
-
- {/* 卡牌预览 */}
-
0 && !store.state.error}>
-
-
-
- {/* 空状态 */}
-
-
- 暂无卡牌数据
-
-
-
-
- {/* 右侧:属性编辑表单 */}
-
-
-
-
- );
-});
diff --git a/src/components/md-deck/CardPreview.tsx b/src/components/md-deck/CardPreview.tsx
new file mode 100644
index 0000000..5f9f2d7
--- /dev/null
+++ b/src/components/md-deck/CardPreview.tsx
@@ -0,0 +1,113 @@
+import { Show, For, createMemo } from 'solid-js';
+import { marked } from '../../markdown';
+import { getLayerStyle } from './hooks/dimensions';
+import { useCardSelection } from './hooks/useCardSelection';
+import { getSelectionBoxStyle } from './hooks/useCardSelection';
+import type { DeckStore } from './hooks/deckStore';
+
+export interface CardPreviewProps {
+ store: DeckStore;
+}
+
+/**
+ * 渲染 layer 内容(提取为纯工具函数)
+ */
+function renderLayerContent(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 { store } = props;
+
+ // 使用 createMemo 优化计算
+ const currentCard = createMemo(() => store.state.cards[store.state.activeTab]);
+ const visibleLayers = createMemo(() => store.state.layerConfigs.filter((l) => l.visible));
+ const selectionStyle = createMemo(() =>
+ getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions)
+ );
+
+ const selection = useCardSelection(store);
+
+ let cardRef: HTMLDivElement | undefined;
+
+ return (
+
+
+ selection.onMouseDown(e, cardRef!)}
+ onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
+ onMouseUp={selection.onMouseUp}
+ onMouseLeave={selection.onMouseLeave}
+ >
+ {/* 框选遮罩 */}
+
+
+
+
+ {/* 网格区域容器 */}
+
+ {/* 编辑模式下的网格线 */}
+
+
+
+ {(_, i) => (
+
+ )}
+
+
+ {(_, i) => (
+
+ )}
+
+
+
+
+ {/* 渲染每个 layer */}
+
+ {(layer) => {
+ const style = getLayerStyle(layer, store.state.dimensions!);
+
+ return (
+
+ );
+ }}
+
+
+
+
+
+ );
+}
diff --git a/src/components/md-deck/DeckContent.tsx b/src/components/md-deck/DeckContent.tsx
new file mode 100644
index 0000000..3790de0
--- /dev/null
+++ b/src/components/md-deck/DeckContent.tsx
@@ -0,0 +1,43 @@
+import { Show } from 'solid-js';
+import { CardPreview } from './CardPreview';
+import type { DeckStore } from './hooks/deckStore';
+
+export interface DeckContentProps {
+ store: DeckStore;
+ isLoading: boolean;
+}
+
+/**
+ * 卡牌预览内容区域:错误/加载/卡牌预览/空状态
+ */
+export function DeckContent(props: DeckContentProps) {
+ return (
+ <>
+ {/* 错误提示 */}
+
+
+ {props.store.state.error}
+
+
+
+ {/* 加载状态 */}
+
+
+ 加载卡牌数据中...
+
+
+
+ {/* 卡牌预览 */}
+ 0 && !props.store.state.error}>
+
+
+
+ {/* 空状态 */}
+
+
+ 暂无卡牌数据
+
+
+ >
+ );
+}
diff --git a/src/components/md-deck/DeckHeader.tsx b/src/components/md-deck/DeckHeader.tsx
new file mode 100644
index 0000000..304fe40
--- /dev/null
+++ b/src/components/md-deck/DeckHeader.tsx
@@ -0,0 +1,47 @@
+import { For } from 'solid-js';
+import type { DeckStore } from './hooks/deckStore';
+
+export interface DeckHeaderProps {
+ store: DeckStore;
+}
+
+/**
+ * 卡牌预览头部:编辑按钮和 Tab 选择器
+ */
+export function DeckHeader(props: DeckHeaderProps) {
+ const { store } = props;
+
+ return (
+
+ {/* 编辑按钮 */}
+
+
+ {/* Tab 选择器 */}
+
+
+ {(card, index) => (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/md-deck/editor-panel/DataEditorPanel.tsx b/src/components/md-deck/editor-panel/DataEditorPanel.tsx
new file mode 100644
index 0000000..7bedd90
--- /dev/null
+++ b/src/components/md-deck/editor-panel/DataEditorPanel.tsx
@@ -0,0 +1,34 @@
+import { For } from 'solid-js';
+import type { DeckStore } from '../hooks/deckStore';
+
+export interface DataEditorPanelProps {
+ activeTab: number;
+ cards: DeckStore['state']['cards'];
+ updateCardData: DeckStore['actions']['updateCardData'];
+}
+
+/**
+ * 左侧:CSV 数据编辑面板
+ */
+export function DataEditorPanel(props: DataEditorPanelProps) {
+ return (
+
+
卡牌数据
+
+
+ {(key) => (
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/editor-panel.tsx b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx
similarity index 74%
rename from src/components/editor-panel.tsx
rename to src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx
index 053dcf5..d742471 100644
--- a/src/components/editor-panel.tsx
+++ b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx
@@ -1,42 +1,10 @@
import { For } from 'solid-js';
-import type { DeckStore } from './stores/deckStore';
-
-export interface DataEditorPanelProps {
- activeTab: number;
- cards: DeckStore['state']['cards'];
- updateCardData: DeckStore['actions']['updateCardData'];
-}
+import type { DeckStore } from '../hooks/deckStore';
export interface PropertiesEditorPanelProps {
store: DeckStore;
}
-/**
- * 左侧:CSV 数据编辑面板
- */
-export function DataEditorPanel(props: DataEditorPanelProps) {
- return (
-
-
卡牌数据
-
-
- {(key) => (
-
-
-
- )}
-
-
-
- );
-}
-
/**
* 右侧:卡牌属性编辑面板
*/
diff --git a/src/components/md-deck/editor-panel/index.ts b/src/components/md-deck/editor-panel/index.ts
new file mode 100644
index 0000000..5b7192c
--- /dev/null
+++ b/src/components/md-deck/editor-panel/index.ts
@@ -0,0 +1,3 @@
+export { DataEditorPanel } from './DataEditorPanel';
+export { PropertiesEditorPanel } from './PropertiesEditorPanel';
+export type { DataEditorPanelProps } from './DataEditorPanel';
diff --git a/src/components/stores/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts
similarity index 74%
rename from src/components/stores/deckStore.ts
rename to src/components/md-deck/hooks/deckStore.ts
index 3ff9fe9..3cf1d9b 100644
--- a/src/components/stores/deckStore.ts
+++ b/src/components/md-deck/hooks/deckStore.ts
@@ -1,7 +1,19 @@
import { createStore } from 'solid-js/store';
-import { calculateDimensions } from '../utils/dimensions';
+import { calculateDimensions } from './dimensions';
+import { loadCSV } from '../../utils/csv-loader';
+import { initLayerConfigs } from './layer-parser';
import type { CardData, LayerConfig, Dimensions } from '../types';
+/**
+ * 默认配置常量
+ */
+export const DECK_DEFAULTS = {
+ SIZE: '54x86',
+ GRID: '5x8',
+ BLEED: '1',
+ PADDING: '2'
+} as const;
+
export interface DeckState {
// 基本属性
size: string;
@@ -30,6 +42,9 @@ export interface DeckState {
selectStart: { x: number; y: number } | null;
selectEnd: { x: number; y: number } | null;
+ // 加载状态
+ isLoading: boolean;
+
// 错误状态
error: string | null;
}
@@ -62,13 +77,14 @@ export interface DeckActions {
setSelectEnd: (pos: { x: number; y: number } | null) => void;
cancelSelection: () => void;
- // 初始化和数据加载
- loadCards: (cards: CardData[]) => void;
+ // 数据加载
+ loadCardsFromPath: (path: string, layersStr?: string) => Promise;
setError: (error: string | null) => void;
+ clearError: () => void;
// 生成代码
generateCode: () => string;
- copyCode: () => void;
+ copyCode: () => Promise;
}
export interface DeckStore {
@@ -79,14 +95,17 @@ export interface DeckStore {
/**
* 创建 deck store
*/
-export function createDeckStore(): DeckStore {
+export function createDeckStore(
+ initialSrc: string = '',
+ initialLayers: string = ''
+): DeckStore {
const [state, setState] = createStore({
- size: '54x86',
- grid: '5x8',
- bleed: '1',
- padding: '2',
+ size: DECK_DEFAULTS.SIZE,
+ grid: DECK_DEFAULTS.GRID,
+ bleed: DECK_DEFAULTS.BLEED,
+ padding: DECK_DEFAULTS.PADDING,
fixed: false,
- src: '',
+ src: initialSrc,
dimensions: null,
cards: [],
activeTab: 0,
@@ -96,6 +115,7 @@ export function createDeckStore(): DeckStore {
isSelecting: false,
selectStart: null,
selectEnd: null,
+ isLoading: false,
error: null
});
@@ -161,17 +181,43 @@ export function createDeckStore(): DeckStore {
setState({ isSelecting: false, selectStart: null, selectEnd: null });
};
- // 加载卡牌数据并初始化 dimensions 和 layerConfigs
- const loadCards = (cards: CardData[]) => {
- if (cards.length === 0) {
- setState({ error: 'CSV 文件为空或格式不正确' });
+ // 加载卡牌数据(核心逻辑)
+ const loadCardsFromPath = async (path: string, layersStr: string = '') => {
+ if (!path) {
+ setState({ error: '未指定 CSV 文件路径' });
return;
}
- setState({ cards, activeTab: 0, error: null });
- updateDimensions();
+
+ setState({ isLoading: true, error: null, src: path });
+
+ try {
+ const data = await loadCSV(path);
+
+ if (data.length === 0) {
+ setState({
+ error: 'CSV 文件为空或格式不正确',
+ isLoading: false
+ });
+ return;
+ }
+
+ setState({
+ cards: data,
+ activeTab: 0,
+ layerConfigs: initLayerConfigs(data, layersStr),
+ isLoading: false
+ });
+ updateDimensions();
+ } catch (err) {
+ setState({
+ error: `加载 CSV 失败:${err instanceof Error ? err.message : '未知错误'}`,
+ isLoading: false
+ });
+ }
};
const setError = (error: string | null) => setState({ error });
+ const clearError = () => setState({ error: null });
const generateCode = () => {
const layersStr = state.layerConfigs
@@ -181,13 +227,15 @@ export function createDeckStore(): DeckStore {
return `:md-deck[${state.src}]{size="${state.size}" grid="${state.grid}" bleed="${state.bleed}" padding="${state.padding}" layers="${layersStr}"}`;
};
- const copyCode = () => {
+ const copyCode = async () => {
const code = generateCode();
- navigator.clipboard.writeText(code).then(() => {
+ try {
+ await navigator.clipboard.writeText(code);
alert('已复制到剪贴板!');
- }).catch(err => {
+ } catch (err) {
console.error('复制失败:', err);
- });
+ alert('复制失败,请手动复制');
+ }
};
const actions: DeckActions = {
@@ -208,8 +256,9 @@ export function createDeckStore(): DeckStore {
setSelectStart,
setSelectEnd,
cancelSelection,
- loadCards,
+ loadCardsFromPath,
setError,
+ clearError,
generateCode,
copyCode
};
diff --git a/src/components/utils/dimensions.ts b/src/components/md-deck/hooks/dimensions.ts
similarity index 100%
rename from src/components/utils/dimensions.ts
rename to src/components/md-deck/hooks/dimensions.ts
diff --git a/src/components/utils/layer-parser.ts b/src/components/md-deck/hooks/layer-parser.ts
similarity index 100%
rename from src/components/utils/layer-parser.ts
rename to src/components/md-deck/hooks/layer-parser.ts
diff --git a/src/components/stores/use-selection.ts b/src/components/md-deck/hooks/useCardSelection.ts
similarity index 90%
rename from src/components/stores/use-selection.ts
rename to src/components/md-deck/hooks/useCardSelection.ts
index 5b448c9..4477ace 100644
--- a/src/components/stores/use-selection.ts
+++ b/src/components/md-deck/hooks/useCardSelection.ts
@@ -2,9 +2,9 @@ import type { DeckStore } from './deckStore';
/**
* 框选相关的操作(已整合到 deckStore)
- * 此 hook 保留用于向后兼容或提取特定逻辑
+ * 此 hook 用于处理卡牌预览区域的鼠标交互
*/
-export function useSelection(store: DeckStore) {
+export function useCardSelection(store: DeckStore) {
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement, dimensions: DeckStore['state']['dimensions']) => {
if (!dimensions) return { gridX: 1, gridY: 1 };
@@ -67,9 +67,9 @@ export function useSelection(store: DeckStore) {
* 计算框选区域的样式
*/
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
+ 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;
diff --git a/src/components/md-deck/index.tsx b/src/components/md-deck/index.tsx
new file mode 100644
index 0000000..ba7b8be
--- /dev/null
+++ b/src/components/md-deck/index.tsx
@@ -0,0 +1,88 @@
+import { customElement, noShadowDOM } from 'solid-element';
+import { Show, createEffect, onCleanup } from 'solid-js';
+import { resolvePath } from '../utils/path';
+import { createDeckStore } from './hooks/deckStore';
+import { DeckHeader } from './DeckHeader';
+import { DeckContent } from './DeckContent';
+import { DataEditorPanel, PropertiesEditorPanel } from './editor-panel';
+
+interface DeckProps {
+ size?: string;
+ grid?: string;
+ bleed?: string;
+ padding?: string;
+ layers?: string;
+ fixed?: boolean | string;
+}
+
+customElement('md-deck', {
+ size: '54x86',
+ grid: '5x8',
+ bleed: '1',
+ padding: '2',
+ layers: '',
+ fixed: false
+}, (props, { element }) => {
+ noShadowDOM();
+
+ // 从 element 的 textContent 获取 CSV 路径
+ const csvPath = element?.textContent?.trim() || '';
+
+ // 隐藏原始文本内容
+ if (element) {
+ element.textContent = '';
+ }
+
+ // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
+ const articleEl = element?.closest('article[data-src]');
+ const articlePath = articleEl?.getAttribute('data-src') || '';
+
+ // 解析相对路径
+ const resolvedSrc = resolvePath(articlePath, csvPath);
+
+ // 创建 store 并加载数据
+ const store = createDeckStore(resolvedSrc, (props.layers as string) || '');
+
+ // 初始化 store 属性
+ 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 数据
+ store.actions.loadCardsFromPath(resolvedSrc, (props.layers as string) || '');
+
+ // 清理函数
+ onCleanup(() => {
+ store.actions.clearError();
+ });
+
+ return (
+
+ {/* 左侧:CSV 数据编辑 */}
+
+
+
+
+ {/* 中间:卡牌预览和控制 */}
+
+ {/* Tab 选择器和编辑按钮 */}
+ 0 && !store.state.error}>
+
+
+
+ {/* 内容区域:错误/加载/卡牌预览/空状态 */}
+
+
+
+ {/* 右侧:属性编辑表单 */}
+
+
+
+
+ );
+});
diff --git a/src/components/types.ts b/src/components/md-deck/types.ts
similarity index 100%
rename from src/components/types.ts
rename to src/components/md-deck/types.ts