2026-03-15 00:52:00 +08:00
|
|
|
|
import { createSignal, For, Show } from 'solid-js';
|
2026-02-27 16:02:53 +08:00
|
|
|
|
import type { DeckStore } from './hooks/deckStore';
|
2026-02-27 21:12:23 +08:00
|
|
|
|
import { usePageLayout } from './hooks/usePageLayout';
|
|
|
|
|
|
import { usePDFExport, type ExportOptions } from './hooks/usePDFExport';
|
2026-03-14 16:20:55 +08:00
|
|
|
|
import { usePlotterExport } from './hooks/usePlotterExport';
|
2026-03-14 15:48:55 +08:00
|
|
|
|
import { getShapeSvgClipPath } from './hooks/shape-styles';
|
2026-02-27 21:12:23 +08:00
|
|
|
|
import { PrintPreviewHeader } from './PrintPreviewHeader';
|
|
|
|
|
|
import { PrintPreviewFooter } from './PrintPreviewFooter';
|
|
|
|
|
|
import { CardLayer } from './CardLayer';
|
2026-03-15 00:52:00 +08:00
|
|
|
|
import { PltPreview } from './PltPreview';
|
2026-02-27 16:02:53 +08:00
|
|
|
|
|
|
|
|
|
|
export interface PrintPreviewProps {
|
|
|
|
|
|
store: DeckStore;
|
|
|
|
|
|
onClose: () => void;
|
2026-02-27 20:27:26 +08:00
|
|
|
|
onExport: () => void;
|
2026-02-27 16:02:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打印预览组件:在 A4 纸张上排列所有卡牌
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function PrintPreview(props: PrintPreviewProps) {
|
|
|
|
|
|
const { store } = props;
|
2026-02-27 21:12:23 +08:00
|
|
|
|
const { getA4Size, pages, cropMarks } = usePageLayout(store);
|
|
|
|
|
|
const { exportToPDF } = usePDFExport(store, props.onClose);
|
2026-03-15 00:52:00 +08:00
|
|
|
|
const { generatePltData, downloadPltFile } = usePlotterExport(store);
|
|
|
|
|
|
|
|
|
|
|
|
const [showPltPreview, setShowPltPreview] = createSignal(false);
|
2026-03-15 09:29:18 +08:00
|
|
|
|
const [pltCode, setPltCode] = createSignal('');
|
2026-02-27 21:12:23 +08:00
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
|
const frontVisibleLayers = () => store.state.frontLayerConfigs.filter((l) => l.visible);
|
|
|
|
|
|
const backVisibleLayers = () => store.state.backLayerConfigs.filter((l) => l.visible);
|
2026-02-27 21:12:23 +08:00
|
|
|
|
|
|
|
|
|
|
const handleExport = async () => {
|
|
|
|
|
|
const options: ExportOptions = {
|
|
|
|
|
|
orientation: store.state.printOrientation,
|
|
|
|
|
|
cardWidth: store.state.dimensions?.cardWidth || 56,
|
|
|
|
|
|
cardHeight: store.state.dimensions?.cardHeight || 88,
|
|
|
|
|
|
gridOriginX: store.state.dimensions?.gridOriginX || 0,
|
|
|
|
|
|
gridOriginY: store.state.dimensions?.gridOriginY || 0,
|
|
|
|
|
|
gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56,
|
|
|
|
|
|
gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88,
|
2026-03-13 17:26:00 +08:00
|
|
|
|
visibleLayers: frontVisibleLayers(),
|
2026-02-27 21:12:23 +08:00
|
|
|
|
dimensions: store.state.dimensions!
|
|
|
|
|
|
};
|
|
|
|
|
|
await exportToPDF(pages(), cropMarks(), options);
|
2026-02-27 18:19:37 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-15 00:52:00 +08:00
|
|
|
|
const handleOpenPltPreview = () => {
|
2026-03-15 09:29:18 +08:00
|
|
|
|
const data = generatePltData();
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
setPltCode(data.pltCode);
|
|
|
|
|
|
setShowPltPreview(true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert('没有可预览的卡片');
|
|
|
|
|
|
}
|
2026-03-15 00:52:00 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClosePltPreview = () => {
|
|
|
|
|
|
setShowPltPreview(false);
|
2026-03-14 16:20:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-27 16:02:53 +08:00
|
|
|
|
return (
|
2026-03-15 09:29:18 +08:00
|
|
|
|
<Show when={!showPltPreview()} fallback={<PltPreview pltCode={pltCode()} cardWidth={store.state.dimensions?.cardWidth || 56} cardHeight={store.state.dimensions?.cardHeight || 88} shape={store.state.shape} bleed={store.state.bleed || 1} cornerRadius={store.state.cornerRadius ?? 3} orientation={store.state.printOrientation || 'landscape'} onClose={handleClosePltPreview} />}>
|
2026-03-15 00:52:00 +08:00
|
|
|
|
<div class="fixed inset-0 bg-black/50 z-50 overflow-auto">
|
|
|
|
|
|
<div class="min-h-screen py-20 px-4">
|
|
|
|
|
|
<PrintPreviewHeader
|
|
|
|
|
|
store={store}
|
|
|
|
|
|
pageCount={pages().length}
|
|
|
|
|
|
onExport={handleExport}
|
|
|
|
|
|
onOpenPltPreview={handleOpenPltPreview}
|
|
|
|
|
|
onClose={props.onClose}
|
|
|
|
|
|
/>
|
2026-02-27 16:02:53 +08:00
|
|
|
|
|
2026-02-27 21:12:23 +08:00
|
|
|
|
<PrintPreviewFooter store={store} />
|
2026-02-27 21:02:33 +08:00
|
|
|
|
|
2026-02-27 20:27:26 +08:00
|
|
|
|
<div class="flex flex-col items-center gap-8">
|
2026-02-27 16:02:53 +08:00
|
|
|
|
<For each={pages()}>
|
2026-03-13 17:26:00 +08:00
|
|
|
|
{(page) => {
|
|
|
|
|
|
// 根据页面类型(正面/背面)决定使用哪个图层配置
|
|
|
|
|
|
const isFrontPage = page.cards[0]?.side !== 'back';
|
|
|
|
|
|
const visibleLayersForPage = isFrontPage ? frontVisibleLayers() : backVisibleLayers();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<svg
|
|
|
|
|
|
class="bg-white shadow-xl"
|
|
|
|
|
|
viewBox={`0 0 ${getA4Size().width}mm ${getA4Size().height}mm`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: `${getA4Size().width}mm`,
|
|
|
|
|
|
height: `${getA4Size().height}mm`
|
|
|
|
|
|
}}
|
|
|
|
|
|
data-page={page.pageIndex + 1}
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
>
|
|
|
|
|
|
<rect
|
|
|
|
|
|
x={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.x}mm`}
|
|
|
|
|
|
y={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.y}mm`}
|
|
|
|
|
|
width={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.width}mm`}
|
|
|
|
|
|
height={`${cropMarks()[page.pageIndex]?.frameBoundsWithMargin.height}mm`}
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="black"
|
|
|
|
|
|
stroke-width="0.2"
|
|
|
|
|
|
/>
|
2026-02-27 18:19:37 +08:00
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
|
<For each={cropMarks()[page.pageIndex]?.horizontalLines}>
|
|
|
|
|
|
{(line) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<line
|
|
|
|
|
|
x1={`${line.xStart}mm`}
|
|
|
|
|
|
y1={`${line.y}mm`}
|
|
|
|
|
|
x2={`${page.frameBounds.minX}mm`}
|
|
|
|
|
|
y2={`${line.y}mm`}
|
|
|
|
|
|
stroke="#888"
|
|
|
|
|
|
stroke-width="0.1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<line
|
|
|
|
|
|
x1={`${page.frameBounds.maxX}mm`}
|
|
|
|
|
|
y1={`${line.y}mm`}
|
|
|
|
|
|
x2={`${line.xEnd}mm`}
|
|
|
|
|
|
y2={`${line.y}mm`}
|
|
|
|
|
|
stroke="#888"
|
|
|
|
|
|
stroke-width="0.1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</For>
|
2026-02-27 21:12:23 +08:00
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
|
<For each={cropMarks()[page.pageIndex]?.verticalLines}>
|
|
|
|
|
|
{(line) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<line
|
|
|
|
|
|
x1={`${line.x}mm`}
|
|
|
|
|
|
y1={`${line.yStart}mm`}
|
|
|
|
|
|
x2={`${line.x}mm`}
|
|
|
|
|
|
y2={`${page.frameBounds.minY}mm`}
|
|
|
|
|
|
stroke="#888"
|
|
|
|
|
|
stroke-width="0.1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<line
|
|
|
|
|
|
x1={`${line.x}mm`}
|
|
|
|
|
|
y1={`${page.frameBounds.maxY}mm`}
|
|
|
|
|
|
x2={`${line.x}mm`}
|
|
|
|
|
|
y2={`${line.yEnd}mm`}
|
|
|
|
|
|
stroke="#888"
|
|
|
|
|
|
stroke-width="0.1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</For>
|
2026-02-27 18:19:37 +08:00
|
|
|
|
|
2026-03-13 17:26:00 +08:00
|
|
|
|
<For each={page.cards}>
|
2026-03-14 15:48:55 +08:00
|
|
|
|
{(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>
|
2026-03-13 17:26:00 +08:00
|
|
|
|
</div>
|
2026-03-14 15:48:55 +08:00
|
|
|
|
</foreignObject>
|
|
|
|
|
|
</g>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
2026-03-13 17:26:00 +08:00
|
|
|
|
</For>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
2026-02-27 16:02:53 +08:00
|
|
|
|
</For>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-15 00:52:00 +08:00
|
|
|
|
</Show>
|
2026-02-27 16:02:53 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|