feat: jsPDF
This commit is contained in:
@@ -25,12 +25,12 @@ export function DeckHeader(props: DeckHeaderProps) {
|
||||
{store.state.isEditing ? '✓ 编辑中' : '✏️ 编辑'}
|
||||
</button>
|
||||
|
||||
{/* 打印按钮 */}
|
||||
{/* 导出 PDF 按钮 */}
|
||||
<button
|
||||
onClick={() => store.actions.printDeck()}
|
||||
onClick={() => store.actions.exportDeck()}
|
||||
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-green-100 text-green-600 hover:bg-green-200"
|
||||
>
|
||||
🖨️ 打印
|
||||
📥 导出 PDF
|
||||
</button>
|
||||
|
||||
{/* Tab 选择器 */}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { For, createMemo } from 'solid-js';
|
||||
import { marked } from '../../markdown';
|
||||
import { getLayerStyle } from './hooks/dimensions';
|
||||
import type { DeckStore } from './hooks/deckStore';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
export interface PrintPreviewProps {
|
||||
store: DeckStore;
|
||||
onClose: () => void;
|
||||
onPrint: () => void;
|
||||
onExport: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +180,120 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||
|
||||
const visibleLayers = createMemo(() => store.state.layerConfigs.filter((l) => l.visible));
|
||||
|
||||
// 导出 PDF
|
||||
const handleExportPDF = async () => {
|
||||
const pagesData = pages();
|
||||
const a4Size = getA4Size();
|
||||
|
||||
// 创建 jsPDF 实例
|
||||
const pdf = new jsPDF({
|
||||
orientation: orientation() === 'landscape' ? 'landscape' : 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
||||
const cardHeight = store.state.dimensions?.cardHeight || 88;
|
||||
const gridOriginX = store.state.dimensions?.gridOriginX || 0;
|
||||
const gridOriginY = store.state.dimensions?.gridOriginY || 0;
|
||||
const gridAreaWidth = store.state.dimensions?.gridAreaWidth || cardWidth;
|
||||
const gridAreaHeight = store.state.dimensions?.gridAreaHeight || cardHeight;
|
||||
const fontSize = store.state.dimensions?.fontSize || 3;
|
||||
|
||||
// 为每页生成内容
|
||||
for (let i = 0; i < pagesData.length; i++) {
|
||||
if (i > 0) {
|
||||
pdf.addPage();
|
||||
}
|
||||
|
||||
const page = pagesData()[i];
|
||||
const cropData = cropMarks()[i];
|
||||
|
||||
// 绘制外围边框
|
||||
const frameMargin = cropData.frameBoundsWithMargin;
|
||||
pdf.setDrawColor(0);
|
||||
pdf.setLineWidth(0.2);
|
||||
pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height);
|
||||
|
||||
// 绘制水平裁切线
|
||||
for (const line of cropData.horizontalLines) {
|
||||
pdf.setDrawColor(136);
|
||||
pdf.setLineWidth(0.1);
|
||||
// 左侧裁切线
|
||||
pdf.line(line.xStart, line.y, page.frameBounds.minX, line.y);
|
||||
// 右侧裁切线
|
||||
pdf.line(page.frameBounds.maxX, line.y, line.xEnd, line.y);
|
||||
}
|
||||
|
||||
// 绘制垂直裁切线
|
||||
for (const line of cropData.verticalLines) {
|
||||
pdf.setDrawColor(136);
|
||||
pdf.setLineWidth(0.1);
|
||||
// 上方裁切线
|
||||
pdf.line(line.x, line.yStart, line.x, page.frameBounds.minY);
|
||||
// 下方裁切线
|
||||
pdf.line(line.x, page.frameBounds.maxY, line.x, line.yEnd);
|
||||
}
|
||||
|
||||
// 渲染卡牌内容
|
||||
for (const card of page.cards) {
|
||||
// 创建临时容器渲染卡牌内容
|
||||
const container = document.createElement('div');
|
||||
container.style.position = 'absolute';
|
||||
container.style.left = '-9999px';
|
||||
container.style.top = '-9999px';
|
||||
container.style.width = `${cardWidth}mm`;
|
||||
container.style.height = `${cardHeight}mm`;
|
||||
container.style.background = 'white';
|
||||
|
||||
// 网格区域容器
|
||||
const gridContainer = document.createElement('div');
|
||||
gridContainer.style.position = 'absolute';
|
||||
gridContainer.style.left = `${gridOriginX}mm`;
|
||||
gridContainer.style.top = `${gridOriginY}mm`;
|
||||
gridContainer.style.width = `${gridAreaWidth}mm`;
|
||||
gridContainer.style.height = `${gridAreaHeight}mm`;
|
||||
|
||||
// 渲染每个 layer
|
||||
for (const layer of visibleLayers()) {
|
||||
const layerEl = document.createElement('div');
|
||||
layerEl.className = 'absolute flex items-center justify-center text-center prose prose-sm';
|
||||
Object.assign(layerEl.style, getLayerStyle(layer, store.state.dimensions!));
|
||||
layerEl.style.fontSize = `${fontSize}mm`;
|
||||
layerEl.innerHTML = renderLayerContent(layer, card.data);
|
||||
gridContainer.appendChild(layerEl);
|
||||
}
|
||||
|
||||
container.appendChild(gridContainer);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// 使用 html2canvas 渲染
|
||||
try {
|
||||
const html2canvas = (await import('html2canvas')).default;
|
||||
const canvas = await html2canvas(container, {
|
||||
scale: 2,
|
||||
backgroundColor: null,
|
||||
logging: false,
|
||||
useCORS: true
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
pdf.addImage(imgData, 'PNG', card.x, card.y, cardWidth, cardHeight);
|
||||
} catch (e) {
|
||||
console.error('渲染卡牌内容失败:', e);
|
||||
}
|
||||
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存 PDF 文件
|
||||
pdf.save('deck.pdf');
|
||||
|
||||
// 关闭预览
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
// 渲染单个卡片的 SVG 内容(使用 foreignObject)
|
||||
const renderCardInSvg = (card: { data: typeof store.state.cards[0]; x: number; y: number }, pageIndex: number) => {
|
||||
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
||||
@@ -230,17 +345,8 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="fixed inset-0 bg-black/50 z-50 overflow-auto print:overflow-visible print:absolute">
|
||||
{/* 打印样式:根据方向设置 @page 规则 */}
|
||||
<style>{`
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 ${orientation() === 'landscape' ? 'landscape' : 'portrait'};
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div class="min-h-screen py-20 px-4 print:p-0">
|
||||
<div class="fixed inset-0 bg-black/50 z-50 overflow-auto">
|
||||
<div class="min-h-screen py-20 px-4">
|
||||
{/* 打印预览控制栏 */}
|
||||
<div class="fixed top-0 left-0 right-0 z-50 bg-white shadow-lg rounded-lg mx-4 mt-4 px-4 py-3 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -303,11 +409,11 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onClick={props.onPrint}
|
||||
onClick={handleExportPDF}
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<span>🖨️</span>
|
||||
<span>打印</span>
|
||||
<span>📥</span>
|
||||
<span>导出 PDF</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
@@ -319,11 +425,11 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||
</div>
|
||||
|
||||
{/* A4 纸张预览:每页都是一个完整的 SVG */}
|
||||
<div class="flex flex-col items-center gap-8 print-root">
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
<svg
|
||||
class="bg-white shadow-xl print:shadow-none"
|
||||
class="bg-white shadow-xl"
|
||||
viewBox={`0 0 ${getA4Size().width}mm ${getA4Size().height}mm`}
|
||||
style={{
|
||||
width: `${getA4Size().width}mm`,
|
||||
|
||||
@@ -54,8 +54,8 @@ export interface DeckState {
|
||||
// 错误状态
|
||||
error: string | null;
|
||||
|
||||
// 打印状态
|
||||
isPrinting: boolean;
|
||||
// 导出状态
|
||||
isExporting: boolean;
|
||||
|
||||
// 打印设置
|
||||
printOrientation: 'portrait' | 'landscape';
|
||||
@@ -103,9 +103,9 @@ export interface DeckActions {
|
||||
generateCode: () => string;
|
||||
copyCode: () => Promise<void>;
|
||||
|
||||
// 打印操作
|
||||
setPrinting: (printing: boolean) => void;
|
||||
printDeck: () => void;
|
||||
// 导出操作
|
||||
setExporting: (exporting: boolean) => void;
|
||||
exportDeck: () => void;
|
||||
|
||||
// 打印设置
|
||||
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
||||
@@ -146,7 +146,7 @@ export function createDeckStore(
|
||||
selectEnd: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isPrinting: false,
|
||||
isExporting: false,
|
||||
printOrientation: 'portrait',
|
||||
printOddPageOffsetX: 0,
|
||||
printOddPageOffsetY: 0
|
||||
@@ -286,10 +286,10 @@ export function createDeckStore(
|
||||
}
|
||||
};
|
||||
|
||||
const setPrinting = (printing: boolean) => setState({ isPrinting: printing });
|
||||
const setExporting = (exporting: boolean) => setState({ isExporting: exporting });
|
||||
|
||||
const printDeck = () => {
|
||||
setState({ isPrinting: true });
|
||||
const exportDeck = () => {
|
||||
setState({ isExporting: true });
|
||||
};
|
||||
|
||||
const setPrintOrientation = (orientation: 'portrait' | 'landscape') => {
|
||||
@@ -330,8 +330,8 @@ export function createDeckStore(
|
||||
clearError,
|
||||
generateCode,
|
||||
copyCode,
|
||||
setPrinting,
|
||||
printDeck,
|
||||
setExporting,
|
||||
exportDeck,
|
||||
setPrintOrientation,
|
||||
setPrintOddPageOffsetX,
|
||||
setPrintOddPageOffsetY
|
||||
|
||||
@@ -103,12 +103,12 @@ customElement<DeckProps>('md-deck', {
|
||||
|
||||
return (
|
||||
<div class="md-deck mb-4">
|
||||
{/* 打印预览弹窗 */}
|
||||
<Show when={store.state.isPrinting}>
|
||||
{/* 导出 PDF 预览弹窗 */}
|
||||
<Show when={store.state.isExporting}>
|
||||
<PrintPreview
|
||||
store={store}
|
||||
onClose={() => store.actions.setPrinting(false)}
|
||||
onPrint={() => window.print()}
|
||||
onClose={() => store.actions.setExporting(false)}
|
||||
onExport={() => {}}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user