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 { Show, For, createMemo } from 'solid-js';
|
||||||
import { useCardSelection } from './hooks/useCardSelection';
|
import { useCardSelection } from './hooks/useCardSelection';
|
||||||
import { getSelectionBoxStyle } from './hooks/useCardSelection';
|
import { getSelectionBoxStyle } from './hooks/useCardSelection';
|
||||||
|
import { getShapeClipPath } from './hooks/shape-styles';
|
||||||
import { CardLayer } from './CardLayer';
|
import { CardLayer } from './CardLayer';
|
||||||
import type { DeckStore } from './hooks/deckStore';
|
import type { DeckStore } from './hooks/deckStore';
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ export function CardPreview(props: CardPreviewProps) {
|
||||||
const selectionStyle = createMemo(() =>
|
const selectionStyle = createMemo(() =>
|
||||||
getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions)
|
getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions)
|
||||||
);
|
);
|
||||||
|
const shapeClipPath = createMemo(() => getShapeClipPath(store.state.shape));
|
||||||
|
|
||||||
const selection = useCardSelection(store);
|
const selection = useCardSelection(store);
|
||||||
|
|
||||||
|
|
@ -32,7 +34,8 @@ export function CardPreview(props: CardPreviewProps) {
|
||||||
classList={{ 'select-none': store.state.isEditing }}
|
classList={{ 'select-none': store.state.isEditing }}
|
||||||
style={{
|
style={{
|
||||||
width: `${store.state.dimensions?.cardWidth}mm`,
|
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!)}
|
onMouseDown={(e) => selection.onMouseDown(e, cardRef!)}
|
||||||
onMouseMove={(e) => selection.onMouseMove(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 type { DeckStore } from './hooks/deckStore';
|
||||||
import { usePageLayout } from './hooks/usePageLayout';
|
import { usePageLayout } from './hooks/usePageLayout';
|
||||||
import { usePDFExport, type ExportOptions } from './hooks/usePDFExport';
|
import { usePDFExport, type ExportOptions } from './hooks/usePDFExport';
|
||||||
|
import { getShapeSvgClipPath } from './hooks/shape-styles';
|
||||||
import { PrintPreviewHeader } from './PrintPreviewHeader';
|
import { PrintPreviewHeader } from './PrintPreviewHeader';
|
||||||
import { PrintPreviewFooter } from './PrintPreviewFooter';
|
import { PrintPreviewFooter } from './PrintPreviewFooter';
|
||||||
import { CardLayer } from './CardLayer';
|
import { CardLayer } from './CardLayer';
|
||||||
|
|
@ -125,13 +126,23 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<For each={page.cards}>
|
<For each={page.cards}>
|
||||||
{(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 (
|
||||||
<g class="card-group">
|
<g class="card-group">
|
||||||
|
<Show when={shapeClipPath}>
|
||||||
|
<defs>{shapeClipPath}</defs>
|
||||||
|
</Show>
|
||||||
<foreignObject
|
<foreignObject
|
||||||
x={`${card.x}mm`}
|
x={`${card.x}mm`}
|
||||||
y={`${card.y}mm`}
|
y={`${card.y}mm`}
|
||||||
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
width={`${cardWidth}mm`}
|
||||||
height={`${store.state.dimensions?.cardHeight || 88}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="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -153,7 +164,8 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
</g>
|
</g>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { DeckStore } from '../hooks/deckStore';
|
import type { DeckStore } from '../hooks/deckStore';
|
||||||
|
import type { CardShape } from '../types';
|
||||||
|
|
||||||
export interface PropertiesEditorPanelProps {
|
export interface PropertiesEditorPanelProps {
|
||||||
store: DeckStore;
|
store: DeckStore;
|
||||||
|
|
@ -100,6 +101,27 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { calculateDimensions } from './dimensions';
|
import { calculateDimensions } from './dimensions';
|
||||||
import { loadCSV, CSV } from '../../utils/csv-loader';
|
import { loadCSV, CSV } from '../../utils/csv-loader';
|
||||||
import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser';
|
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;
|
gridH: number;
|
||||||
bleed: number;
|
bleed: number;
|
||||||
padding: number;
|
padding: number;
|
||||||
|
shape: CardShape;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
src: string;
|
src: string;
|
||||||
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
||||||
|
|
@ -75,6 +76,7 @@ export interface DeckActions {
|
||||||
setGridH: (grid: number) => void;
|
setGridH: (grid: number) => void;
|
||||||
setBleed: (bleed: number) => void;
|
setBleed: (bleed: number) => void;
|
||||||
setPadding: (padding: number) => void;
|
setPadding: (padding: number) => void;
|
||||||
|
setShape: (shape: CardShape) => void;
|
||||||
|
|
||||||
// 数据设置
|
// 数据设置
|
||||||
setCards: (cards: CSV<CardData>) => void;
|
setCards: (cards: CSV<CardData>) => void;
|
||||||
|
|
@ -144,6 +146,7 @@ export function createDeckStore(
|
||||||
gridH: DECK_DEFAULTS.GRID_H,
|
gridH: DECK_DEFAULTS.GRID_H,
|
||||||
bleed: DECK_DEFAULTS.BLEED,
|
bleed: DECK_DEFAULTS.BLEED,
|
||||||
padding: DECK_DEFAULTS.PADDING,
|
padding: DECK_DEFAULTS.PADDING,
|
||||||
|
shape: 'rectangle',
|
||||||
fixed: false,
|
fixed: false,
|
||||||
src: initialSrc,
|
src: initialSrc,
|
||||||
rawSrc: initialSrc,
|
rawSrc: initialSrc,
|
||||||
|
|
@ -206,6 +209,9 @@ export function createDeckStore(
|
||||||
setState({ padding });
|
setState({ padding });
|
||||||
updateDimensions();
|
updateDimensions();
|
||||||
};
|
};
|
||||||
|
const setShape = (shape: CardShape) => {
|
||||||
|
setState({ shape });
|
||||||
|
};
|
||||||
|
|
||||||
const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
|
const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
|
||||||
const setActiveTab = (index: number) => setState({ activeTab: index });
|
const setActiveTab = (index: number) => setState({ activeTab: index });
|
||||||
|
|
@ -313,6 +319,9 @@ export function createDeckStore(
|
||||||
if (state.padding !== DECK_DEFAULTS.PADDING) {
|
if (state.padding !== DECK_DEFAULTS.PADDING) {
|
||||||
parts.push(`padding="${state.padding}" `);
|
parts.push(`padding="${state.padding}" `);
|
||||||
}
|
}
|
||||||
|
if (state.shape !== 'rectangle') {
|
||||||
|
parts.push(`shape="${state.shape}" `);
|
||||||
|
}
|
||||||
|
|
||||||
parts.push(`layers="${frontLayersStr}" `);
|
parts.push(`layers="${frontLayersStr}" `);
|
||||||
if (backLayersString) {
|
if (backLayersString) {
|
||||||
|
|
@ -368,6 +377,7 @@ export function createDeckStore(
|
||||||
setGridH,
|
setGridH,
|
||||||
setBleed,
|
setBleed,
|
||||||
setPadding,
|
setPadding,
|
||||||
|
setShape,
|
||||||
setCards,
|
setCards,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
updateCardData,
|
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 { Show, onCleanup } from 'solid-js';
|
||||||
import { resolvePath } from '../utils/path';
|
import { resolvePath } from '../utils/path';
|
||||||
import { createDeckStore } from './hooks/deckStore';
|
import { createDeckStore } from './hooks/deckStore';
|
||||||
|
import type { CardShape } from './types';
|
||||||
import { DeckHeader } from './DeckHeader';
|
import { DeckHeader } from './DeckHeader';
|
||||||
import { DeckContent } from './DeckContent';
|
import { DeckContent } from './DeckContent';
|
||||||
import { PrintPreview } from './PrintPreview';
|
import { PrintPreview } from './PrintPreview';
|
||||||
|
|
@ -16,6 +17,7 @@ interface DeckProps {
|
||||||
gridH?: number;
|
gridH?: number;
|
||||||
bleed?: number | string;
|
bleed?: number | string;
|
||||||
padding?: number | string;
|
padding?: number | string;
|
||||||
|
shape?: CardShape;
|
||||||
layers?: string;
|
layers?: string;
|
||||||
backLayers?: string;
|
backLayers?: string;
|
||||||
fixed?: boolean | string;
|
fixed?: boolean | string;
|
||||||
|
|
@ -30,6 +32,7 @@ customElement<DeckProps>('md-deck', {
|
||||||
gridH: 8,
|
gridH: 8,
|
||||||
bleed: 1,
|
bleed: 1,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
|
shape: 'rectangle',
|
||||||
layers: '',
|
layers: '',
|
||||||
backLayers: '',
|
backLayers: '',
|
||||||
fixed: false
|
fixed: false
|
||||||
|
|
@ -87,6 +90,9 @@ customElement<DeckProps>('md-deck', {
|
||||||
store.actions.setPadding(props.padding ?? 2);
|
store.actions.setPadding(props.padding ?? 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置形状
|
||||||
|
store.actions.setShape(props.shape ?? 'rectangle');
|
||||||
|
|
||||||
// 加载 CSV 数据
|
// 加载 CSV 数据
|
||||||
store.actions.loadCardsFromPath(
|
store.actions.loadCardsFromPath(
|
||||||
resolvedSrc,
|
resolvedSrc,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ export interface CardData {
|
||||||
|
|
||||||
export type CardSide = 'front' | 'back';
|
export type CardSide = 'front' | 'back';
|
||||||
|
|
||||||
|
export type CardShape = 'rectangle' | 'circle' | 'triangle' | 'hexagon';
|
||||||
|
|
||||||
export interface Layer {
|
export interface Layer {
|
||||||
prop: string;
|
prop: string;
|
||||||
x1: number;
|
x1: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue