feat: shape for cards
This commit is contained in:
parent
984b8aa1c8
commit
107e6fd6a2
|
|
@ -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!)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</For>
|
||||
|
||||
<For each={page.cards}>
|
||||
{(card) => (
|
||||
<g class="card-group">
|
||||
<foreignObject
|
||||
x={`${card.x}mm`}
|
||||
y={`${card.y}mm`}
|
||||
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
||||
height={`${store.state.dimensions?.cardHeight || 88}mm`}
|
||||
>
|
||||
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
|
||||
<div
|
||||
class="absolute"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${store.state.dimensions?.gridOriginX}mm`,
|
||||
top: `${store.state.dimensions?.gridOriginY}mm`,
|
||||
width: `${store.state.dimensions?.gridAreaWidth}mm`,
|
||||
height: `${store.state.dimensions?.gridAreaHeight}mm`
|
||||
}}
|
||||
>
|
||||
<CardLayer
|
||||
store={store}
|
||||
cardData={card.data}
|
||||
side={card.side || 'front'}
|
||||
/>
|
||||
{(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 (
|
||||
<g class="card-group">
|
||||
<Show when={shapeClipPath}>
|
||||
<defs>{shapeClipPath}</defs>
|
||||
</Show>
|
||||
<foreignObject
|
||||
x={`${card.x}mm`}
|
||||
y={`${card.y}mm`}
|
||||
width={`${cardWidth}mm`}
|
||||
height={`${cardHeight}mm`}
|
||||
clip-path={shapeClipPath ? `url(#${clipPathId})` : undefined}
|
||||
>
|
||||
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
|
||||
<div
|
||||
class="absolute"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${store.state.dimensions?.gridOriginX}mm`,
|
||||
top: `${store.state.dimensions?.gridOriginY}mm`,
|
||||
width: `${store.state.dimensions?.gridAreaWidth}mm`,
|
||||
height: `${store.state.dimensions?.gridAreaHeight}mm`
|
||||
}}
|
||||
>
|
||||
<CardLayer
|
||||
store={store}
|
||||
cardData={card.data}
|
||||
side={card.side || 'front'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
)}
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">卡片形状</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{(['rectangle', 'circle', 'triangle', 'hexagon'] as CardShape[]).map((shape) => (
|
||||
<button
|
||||
onClick={() => store.actions.setShape(shape)}
|
||||
class={`px-3 py-1.5 rounded text-sm font-medium cursor-pointer border ${
|
||||
store.state.shape === shape
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{shape === 'rectangle' && '矩形'}
|
||||
{shape === 'circle' && '圆形'}
|
||||
{shape === 'triangle' && '三角形'}
|
||||
{shape === 'hexagon' && '六边形'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<CardData>) => 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<CardData>) => 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,
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<clipPath id="${id}">
|
||||
<circle cx="${halfW}" cy="${halfH}" r="${halfW}" />
|
||||
</clipPath>`;
|
||||
case 'triangle':
|
||||
return `
|
||||
<clipPath id="${id}">
|
||||
<polygon points="${halfW},0 0,${height} ${width},${height}" />
|
||||
</clipPath>`;
|
||||
case 'hexagon':
|
||||
return `
|
||||
<clipPath id="${id}">
|
||||
<polygon points="${halfW},0 ${width},${height * 0.25} ${width},${height * 0.75} ${halfW},${height} 0,${height * 0.75} 0,${height * 0.25}" />
|
||||
</clipPath>`;
|
||||
case 'rectangle':
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DeckProps>('md-deck', {
|
|||
gridH: 8,
|
||||
bleed: 1,
|
||||
padding: 2,
|
||||
shape: 'rectangle',
|
||||
layers: '',
|
||||
backLayers: '',
|
||||
fixed: false
|
||||
|
|
@ -87,6 +90,9 @@ customElement<DeckProps>('md-deck', {
|
|||
store.actions.setPadding(props.padding ?? 2);
|
||||
}
|
||||
|
||||
// 设置形状
|
||||
store.actions.setShape(props.shape ?? 'rectangle');
|
||||
|
||||
// 加载 CSV 数据
|
||||
store.actions.loadCardsFromPath(
|
||||
resolvedSrc,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue