refactor: remove jspdf dewp

This commit is contained in:
hyper 2026-02-27 22:18:16 +08:00
parent da1e11fb74
commit 806747833e
1 changed files with 168 additions and 120 deletions

View File

@ -1,24 +1,5 @@
import { marked } from '../../../markdown';
import { getLayerStyle } from './dimensions';
import type { DeckStore } from './deckStore';
import type { CardData, LayerConfig, Dimensions } from '../types';
import jsPDF from 'jspdf';
/**
* body {{prop}} markdown
*/
function processBody(body: string, currentRow: CardData): string {
const processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || '');
return marked.parse(processedBody) as string;
}
/**
* layer
*/
function renderLayerContent(layer: { prop: string }, cardData: CardData): string {
const content = cardData[layer.prop] || '';
return processBody(content, cardData);
}
export interface PageCard {
data: CardData;
@ -53,84 +34,12 @@ export interface ExportOptions {
dimensions: Dimensions;
}
/**
* canvas
*/
async function renderCardToCanvas(card: PageCard, options: ExportOptions): Promise<HTMLCanvasElement> {
const { cardWidth, cardHeight, gridOriginX, gridOriginY, gridAreaWidth, gridAreaHeight, fontSize, visibleLayers, dimensions } = options;
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`;
for (const layer of visibleLayers) {
const layerEl = document.createElement('article');
layerEl.className = 'absolute flex items-center justify-center text-center prose prose-sm';
Object.assign(layerEl.style, getLayerStyle(layer, dimensions));
layerEl.style.fontSize = `${fontSize}mm`;
layerEl.innerHTML = renderLayerContent(layer, card.data);
gridContainer.appendChild(layerEl);
}
container.appendChild(gridContainer);
document.body.appendChild(container);
try {
const html2canvas = (await import('html2canvas')).default;
return await html2canvas(container, {
scale: 2,
backgroundColor: null,
logging: false,
useCORS: true,
});
} finally {
document.body.removeChild(container);
}
}
/**
* 线
*/
function drawPageMarks(pdf: jsPDF, cropData: CropMarkData, pageFrameBounds: { minX: number; maxX: number; minY: number; maxY: number }) {
const frameMargin = cropData.frameBoundsWithMargin;
// 外围边框
pdf.setDrawColor(0);
pdf.setLineWidth(0.2);
pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height);
// 水平裁切线
pdf.setDrawColor(136);
pdf.setLineWidth(0.1);
for (const line of cropData.horizontalLines) {
pdf.line(line.xStart, line.y, pageFrameBounds.minX, line.y);
pdf.line(pageFrameBounds.maxX, line.y, line.xEnd, line.y);
}
// 垂直裁切线
for (const line of cropData.verticalLines) {
pdf.line(line.x, line.yStart, line.x, pageFrameBounds.minY);
pdf.line(line.x, pageFrameBounds.maxY, line.x, line.yEnd);
}
}
export interface UsePDFExportReturn {
exportToPDF: (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => Promise<void>;
}
/**
* PDF hook
* PDF hook - HTML
*/
export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExportReturn {
const exportToPDF = async (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => {
@ -140,37 +49,176 @@ export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExpor
store.actions.setExportError(null);
try {
const pdf = new jsPDF({
orientation: options.orientation,
unit: 'mm',
format: 'a4'
});
const a4Width = options.orientation === 'landscape' ? 297 : 210;
const a4Height = options.orientation === 'landscape' ? 210 : 297;
for (let i = 0; i < totalPages; i++) {
if (i > 0) {
pdf.addPage();
}
const page = pages[i];
const cropData = cropMarks[i];
drawPageMarks(pdf, cropData, page.frameBounds);
const totalCards = page.cards.length;
for (let j = 0; j < totalCards; j++) {
const card = page.cards[j];
const canvas = await renderCardToCanvas(card, options);
const imgData = canvas.toDataURL('image/png');
pdf.addImage(imgData, 'PNG', card.x, card.y, options.cardWidth, options.cardHeight);
const currentCardIndex = i * totalCards + j + 1;
const totalCardCount = totalPages * totalCards;
const progress = Math.round((currentCardIndex / totalCardCount) * 100);
store.actions.setExportProgress(progress);
}
// 创建新窗口
const printWindow = window.open('', '_blank');
if (!printWindow) {
throw new Error('无法打开新窗口,请允许弹出窗口');
}
pdf.save('deck.pdf');
// 收集当前文档的所有样式
const styles = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'))
.map(el => el.outerHTML)
.join('\n');
// 为每个页面生成 HTML 内容
const pagesHtml = pages.map((page, pageIndex) => {
const cropData = cropMarks[pageIndex];
const frameMargin = cropData.frameBoundsWithMargin;
// 生成裁切线 HTML
const horizontalLinesHtml = cropData.horizontalLines.map(line => `
<div class="crop-line crop-line-h" style="
position: absolute;
left: ${line.xStart}mm;
top: ${line.y}mm;
width: ${line.xEnd - line.xStart}mm;
height: 0.1mm;
background: #888;
"></div>
`).join('');
const verticalLinesHtml = cropData.verticalLines.map(line => `
<div class="crop-line crop-line-v" style="
position: absolute;
left: ${line.x}mm;
top: ${line.yStart}mm;
width: 0.1mm;
height: ${line.yEnd - line.yStart}mm;
background: #888;
"></div>
`).join('');
// 生成卡牌 HTML
const cardsHtml = page.cards.map((card, cardIndex) => {
// 从 DOM 中获取卡牌内容
const pageSvg = document.querySelector(`svg[data-page="${page.pageIndex + 1}"]`);
const cardGroups = pageSvg?.querySelectorAll('.card-group');
const cardGroup = cardGroups?.[cardIndex] as SVGGElement;
let cardContent = '';
if (cardGroup) {
const foreignObject = cardGroup.querySelector('foreignObject');
if (foreignObject) {
const innerDiv = foreignObject.querySelector('div');
if (innerDiv) {
const cardLayer = innerDiv.querySelector('.absolute');
if (cardLayer) {
cardContent = cardLayer.innerHTML;
}
}
}
}
return `
<div class="card" style="
position: absolute;
left: ${card.x}mm;
top: ${card.y}mm;
width: ${options.cardWidth}mm;
height: ${options.cardHeight}mm;
background: white;
z-index: 1;
">
<div class="card-content" style="
position: absolute;
left: ${options.gridOriginX}mm;
top: ${options.gridOriginY}mm;
width: ${options.gridAreaWidth}mm;
height: ${options.gridAreaHeight}mm;
">
${cardContent}
</div>
</div>
`;
}).join('\n');
return `
<div class="page" style="position: relative;">
<div class="page-frame" style="
position: absolute;
left: ${frameMargin.x}mm;
top: ${frameMargin.y}mm;
width: ${frameMargin.width}mm;
height: ${frameMargin.height}mm;
border: 0.2mm solid black;
box-sizing: border-box;
background: white;
z-index: 1;
"></div>
${horizontalLinesHtml}
${verticalLinesHtml}
${cardsHtml}
</div>
`;
}).join('\n');
// 写入 HTML 内容
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title></title>
${styles}
<style>
@media print {
@page {
size: ${options.orientation === 'landscape' ? 'landscape' : 'portrait'};
margin: 0;
}
body {
margin: 0;
padding: 0;
}
.page {
break-after: page;
page-break-after: always;
}
.page:last-child {
break-after: auto;
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.page {
width: ${a4Width}mm;
height: ${a4Height}mm;
margin-bottom: 20px;
page-break-after: always;
background: white;
}
.page:last-child {
margin-bottom: 0;
page-break-after: auto;
}
.card-content {
position: absolute;
}
.card-content * {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
${pagesHtml}
</body>
</html>
`);
printWindow.document.close();
// 等待内容加载完成后自动打印
printWindow.onload = () => {
store.actions.setExportProgress(100);
};
// 关闭预览
onClose();
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误';