refactor: remove jspdf dewp
This commit is contained in:
parent
da1e11fb74
commit
806747833e
|
|
@ -1,24 +1,5 @@
|
||||||
import { marked } from '../../../markdown';
|
|
||||||
import { getLayerStyle } from './dimensions';
|
|
||||||
import type { DeckStore } from './deckStore';
|
import type { DeckStore } from './deckStore';
|
||||||
import type { CardData, LayerConfig, Dimensions } from '../types';
|
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 {
|
export interface PageCard {
|
||||||
data: CardData;
|
data: CardData;
|
||||||
|
|
@ -53,84 +34,12 @@ export interface ExportOptions {
|
||||||
dimensions: Dimensions;
|
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 {
|
export interface UsePDFExportReturn {
|
||||||
exportToPDF: (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => Promise<void>;
|
exportToPDF: (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PDF 导出 hook
|
* PDF 导出 hook - 打开新窗口并打印 HTML 页面
|
||||||
*/
|
*/
|
||||||
export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExportReturn {
|
export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExportReturn {
|
||||||
const exportToPDF = async (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => {
|
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);
|
store.actions.setExportError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pdf = new jsPDF({
|
const a4Width = options.orientation === 'landscape' ? 297 : 210;
|
||||||
orientation: options.orientation,
|
const a4Height = options.orientation === 'landscape' ? 210 : 297;
|
||||||
unit: 'mm',
|
|
||||||
format: 'a4'
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
// 创建新窗口
|
||||||
if (i > 0) {
|
const printWindow = window.open('', '_blank');
|
||||||
pdf.addPage();
|
if (!printWindow) {
|
||||||
|
throw new Error('无法打开新窗口,请允许弹出窗口');
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = pages[i];
|
// 收集当前文档的所有样式
|
||||||
const cropData = cropMarks[i];
|
const styles = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'))
|
||||||
|
.map(el => el.outerHTML)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
drawPageMarks(pdf, cropData, page.frameBounds);
|
// 为每个页面生成 HTML 内容
|
||||||
|
const pagesHtml = pages.map((page, pageIndex) => {
|
||||||
|
const cropData = cropMarks[pageIndex];
|
||||||
|
const frameMargin = cropData.frameBoundsWithMargin;
|
||||||
|
|
||||||
const totalCards = page.cards.length;
|
// 生成裁切线 HTML
|
||||||
for (let j = 0; j < totalCards; j++) {
|
const horizontalLinesHtml = cropData.horizontalLines.map(line => `
|
||||||
const card = page.cards[j];
|
<div class="crop-line crop-line-h" style="
|
||||||
const canvas = await renderCardToCanvas(card, options);
|
position: absolute;
|
||||||
const imgData = canvas.toDataURL('image/png');
|
left: ${line.xStart}mm;
|
||||||
pdf.addImage(imgData, 'PNG', card.x, card.y, options.cardWidth, options.cardHeight);
|
top: ${line.y}mm;
|
||||||
|
width: ${line.xEnd - line.xStart}mm;
|
||||||
|
height: 0.1mm;
|
||||||
|
background: #888;
|
||||||
|
"></div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
const currentCardIndex = i * totalCards + j + 1;
|
const verticalLinesHtml = cropData.verticalLines.map(line => `
|
||||||
const totalCardCount = totalPages * totalCards;
|
<div class="crop-line crop-line-v" style="
|
||||||
const progress = Math.round((currentCardIndex / totalCardCount) * 100);
|
position: absolute;
|
||||||
store.actions.setExportProgress(progress);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pdf.save('deck.pdf');
|
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();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误';
|
const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue