From 107e6fd6a2143e63d64e7993f1c94ee3ddcf28a8 Mon Sep 17 00:00:00 2001 From: hyper Date: Sat, 14 Mar 2026 15:48:55 +0800 Subject: [PATCH] feat: shape for cards --- src/components/md-deck/CardPreview.tsx | 5 +- src/components/md-deck/PrintPreview.tsx | 70 +++++++++++-------- .../editor-panel/PropertiesEditorPanel.tsx | 22 ++++++ src/components/md-deck/hooks/deckStore.ts | 12 +++- src/components/md-deck/hooks/shape-styles.ts | 56 +++++++++++++++ src/components/md-deck/index.tsx | 6 ++ src/components/md-deck/types.ts | 2 + 7 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 src/components/md-deck/hooks/shape-styles.ts diff --git a/src/components/md-deck/CardPreview.tsx b/src/components/md-deck/CardPreview.tsx index 52aea35..0a450bf 100644 --- a/src/components/md-deck/CardPreview.tsx +++ b/src/components/md-deck/CardPreview.tsx @@ -1,6 +1,7 @@ import { Show, For, createMemo } from 'solid-js'; import { useCardSelection } from './hooks/useCardSelection'; import { getSelectionBoxStyle } from './hooks/useCardSelection'; +import { getShapeClipPath } from './hooks/shape-styles'; import { CardLayer } from './CardLayer'; import type { DeckStore } from './hooks/deckStore'; @@ -18,6 +19,7 @@ export function CardPreview(props: CardPreviewProps) { const selectionStyle = createMemo(() => getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions) ); + const shapeClipPath = createMemo(() => getShapeClipPath(store.state.shape)); const selection = useCardSelection(store); @@ -32,7 +34,8 @@ export function CardPreview(props: CardPreviewProps) { classList={{ 'select-none': store.state.isEditing }} style={{ width: `${store.state.dimensions?.cardWidth}mm`, - height: `${store.state.dimensions?.cardHeight}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!)} diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx index d575179..13682a2 100644 --- a/src/components/md-deck/PrintPreview.tsx +++ b/src/components/md-deck/PrintPreview.tsx @@ -1,7 +1,8 @@ -import { For } from 'solid-js'; +import { For, Show } from 'solid-js'; import type { DeckStore } from './hooks/deckStore'; import { usePageLayout } from './hooks/usePageLayout'; import { usePDFExport, type ExportOptions } from './hooks/usePDFExport'; +import { getShapeSvgClipPath } from './hooks/shape-styles'; import { PrintPreviewHeader } from './PrintPreviewHeader'; import { PrintPreviewFooter } from './PrintPreviewFooter'; import { CardLayer } from './CardLayer'; @@ -125,35 +126,46 @@ export function PrintPreview(props: PrintPreviewProps) { - {(card) => ( - - -
-
- + {(card) => { + const cardWidth = store.state.dimensions?.cardWidth || 56; + const cardHeight = store.state.dimensions?.cardHeight || 88; + const clipPathId = `clip-${page.pageIndex}-${card.data.id || card.x}-${card.y}`; + const shapeClipPath = getShapeSvgClipPath(clipPathId, cardWidth, cardHeight, store.state.shape); + + return ( + + + {shapeClipPath} + + +
+
+ +
-
- - - )} + + + ); + }} ); diff --git a/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx index 4298b26..c513c34 100644 --- a/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx +++ b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx @@ -1,4 +1,5 @@ import type { DeckStore } from '../hooks/deckStore'; +import type { CardShape } from '../types'; export interface PropertiesEditorPanelProps { store: DeckStore; @@ -100,6 +101,27 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) { />
+ +
+ +
+ {(['rectangle', 'circle', 'triangle', 'hexagon'] as CardShape[]).map((shape) => ( + + ))} +
+
); diff --git a/src/components/md-deck/hooks/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts index 593a6e6..626e2f0 100644 --- a/src/components/md-deck/hooks/deckStore.ts +++ b/src/components/md-deck/hooks/deckStore.ts @@ -2,7 +2,7 @@ import { calculateDimensions } from './dimensions'; import { loadCSV, CSV } from '../../utils/csv-loader'; import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser'; -import type { CardData, LayerConfig, Dimensions, CardSide } from '../types'; +import type { CardData, LayerConfig, Dimensions, CardSide, CardShape } from '../types'; /** * 默认配置常量 @@ -24,6 +24,7 @@ export interface DeckState { gridH: number; bleed: number; padding: number; + shape: CardShape; fixed: boolean; src: string; rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径) @@ -75,6 +76,7 @@ export interface DeckActions { setGridH: (grid: number) => void; setBleed: (bleed: number) => void; setPadding: (padding: number) => void; + setShape: (shape: CardShape) => void; // 数据设置 setCards: (cards: CSV) => void; @@ -144,6 +146,7 @@ export function createDeckStore( gridH: DECK_DEFAULTS.GRID_H, bleed: DECK_DEFAULTS.BLEED, padding: DECK_DEFAULTS.PADDING, + shape: 'rectangle', fixed: false, src: initialSrc, rawSrc: initialSrc, @@ -206,6 +209,9 @@ export function createDeckStore( setState({ padding }); updateDimensions(); }; + const setShape = (shape: CardShape) => { + setState({ shape }); + }; const setCards = (cards: CSV) => setState({ cards, activeTab: 0 }); const setActiveTab = (index: number) => setState({ activeTab: index }); @@ -313,6 +319,9 @@ export function createDeckStore( if (state.padding !== DECK_DEFAULTS.PADDING) { parts.push(`padding="${state.padding}" `); } + if (state.shape !== 'rectangle') { + parts.push(`shape="${state.shape}" `); + } parts.push(`layers="${frontLayersStr}" `); if (backLayersString) { @@ -368,6 +377,7 @@ export function createDeckStore( setGridH, setBleed, setPadding, + setShape, setCards, setActiveTab, updateCardData, diff --git a/src/components/md-deck/hooks/shape-styles.ts b/src/components/md-deck/hooks/shape-styles.ts new file mode 100644 index 0000000..cd5bb8f --- /dev/null +++ b/src/components/md-deck/hooks/shape-styles.ts @@ -0,0 +1,56 @@ +import type { CardShape } from '../types'; + +/** + * 获取 CSS clip-path 值用于形状裁剪 + */ +export function getShapeClipPath(shape: CardShape): string { + switch (shape) { + case 'circle': + return 'circle(50% at 50% 50%)'; + case 'triangle': + return 'polygon(50% 0%, 0% 100%, 100% 100%)'; + case 'hexagon': + return 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'; + case 'rectangle': + default: + return 'none'; + } +} + +/** + * 获取 SVG clipPath 定义 + * @param id clipPath 的唯一 ID + * @param width 卡片宽度 + * @param height 卡片高度 + * @param shape 卡片形状 + */ +export function getShapeSvgClipPath( + id: string, + width: number, + height: number, + shape: CardShape +): string { + const halfW = width / 2; + const halfH = height / 2; + + switch (shape) { + case 'circle': + return ` + + + `; + case 'triangle': + return ` + + + `; + case 'hexagon': + return ` + + + `; + case 'rectangle': + default: + return ''; + } +} diff --git a/src/components/md-deck/index.tsx b/src/components/md-deck/index.tsx index 639ab02..aeb0cb6 100644 --- a/src/components/md-deck/index.tsx +++ b/src/components/md-deck/index.tsx @@ -2,6 +2,7 @@ import { customElement, noShadowDOM } from 'solid-element'; import { Show, onCleanup } from 'solid-js'; import { resolvePath } from '../utils/path'; import { createDeckStore } from './hooks/deckStore'; +import type { CardShape } from './types'; import { DeckHeader } from './DeckHeader'; import { DeckContent } from './DeckContent'; import { PrintPreview } from './PrintPreview'; @@ -16,6 +17,7 @@ interface DeckProps { gridH?: number; bleed?: number | string; padding?: number | string; + shape?: CardShape; layers?: string; backLayers?: string; fixed?: boolean | string; @@ -30,6 +32,7 @@ customElement('md-deck', { gridH: 8, bleed: 1, padding: 2, + shape: 'rectangle', layers: '', backLayers: '', fixed: false @@ -87,6 +90,9 @@ customElement('md-deck', { store.actions.setPadding(props.padding ?? 2); } + // 设置形状 + store.actions.setShape(props.shape ?? 'rectangle'); + // 加载 CSV 数据 store.actions.loadCardsFromPath( resolvedSrc, diff --git a/src/components/md-deck/types.ts b/src/components/md-deck/types.ts index 660286a..42c1a87 100644 --- a/src/components/md-deck/types.ts +++ b/src/components/md-deck/types.ts @@ -4,6 +4,8 @@ export interface CardData { export type CardSide = 'front' | 'back'; +export type CardShape = 'rectangle' | 'circle' | 'triangle' | 'hexagon'; + export interface Layer { prop: string; x1: number;