feat: error handling
This commit is contained in:
parent
24c797abc6
commit
47c15e933c
|
|
@ -195,114 +195,134 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
const pagesData = pages();
|
const pagesData = pages();
|
||||||
const a4Size = getA4Size();
|
const a4Size = getA4Size();
|
||||||
|
const totalPages = pagesData.length;
|
||||||
|
|
||||||
// 创建 jsPDF 实例
|
// 重置状态
|
||||||
const pdf = new jsPDF({
|
store.actions.setExportProgress(0);
|
||||||
orientation: orientation() === 'landscape' ? 'landscape' : 'portrait',
|
store.actions.setExportError(null);
|
||||||
unit: 'mm',
|
|
||||||
format: 'a4'
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
try {
|
||||||
const cardHeight = store.state.dimensions?.cardHeight || 88;
|
// 创建 jsPDF 实例
|
||||||
const gridOriginX = store.state.dimensions?.gridOriginX || 0;
|
const pdf = new jsPDF({
|
||||||
const gridOriginY = store.state.dimensions?.gridOriginY || 0;
|
orientation: orientation() === 'landscape' ? 'landscape' : 'portrait',
|
||||||
const gridAreaWidth = store.state.dimensions?.gridAreaWidth || cardWidth;
|
unit: 'mm',
|
||||||
const gridAreaHeight = store.state.dimensions?.gridAreaHeight || cardHeight;
|
format: 'a4'
|
||||||
const fontSize = store.state.dimensions?.fontSize || 3;
|
});
|
||||||
|
|
||||||
// 为每页生成内容
|
const cardWidth = store.state.dimensions?.cardWidth || 56;
|
||||||
for (let i = 0; i < pagesData.length; i++) {
|
const cardHeight = store.state.dimensions?.cardHeight || 88;
|
||||||
if (i > 0) {
|
const gridOriginX = store.state.dimensions?.gridOriginX || 0;
|
||||||
pdf.addPage();
|
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;
|
||||||
|
|
||||||
const page = pagesData[i];
|
// 为每页生成内容
|
||||||
const cropData = cropMarks()[i];
|
for (let i = 0; i < totalPages; i++) {
|
||||||
|
if (i > 0) {
|
||||||
// 绘制外围边框
|
pdf.addPage();
|
||||||
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);
|
const page = pagesData[i];
|
||||||
document.body.appendChild(container);
|
const cropData = cropMarks()[i];
|
||||||
|
|
||||||
// 使用 html2canvas 渲染
|
// 绘制外围边框
|
||||||
try {
|
const frameMargin = cropData.frameBoundsWithMargin;
|
||||||
const html2canvas = (await import('html2canvas')).default;
|
pdf.setDrawColor(0);
|
||||||
const canvas = await html2canvas(container, {
|
pdf.setLineWidth(0.2);
|
||||||
scale: 2,
|
pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height);
|
||||||
backgroundColor: null,
|
|
||||||
logging: false,
|
|
||||||
useCORS: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const imgData = canvas.toDataURL('image/png');
|
// 绘制水平裁切线
|
||||||
pdf.addImage(imgData, 'PNG', card.x, card.y, cardWidth, cardHeight);
|
for (const line of cropData.horizontalLines) {
|
||||||
} catch (e) {
|
pdf.setDrawColor(136);
|
||||||
console.error('渲染卡牌内容失败:', e);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(container);
|
// 绘制垂直裁切线
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染卡牌内容
|
||||||
|
const totalCards = page.cards.length;
|
||||||
|
for (let j = 0; j < totalCards; j++) {
|
||||||
|
const card = page.cards[j];
|
||||||
|
|
||||||
|
// 创建临时容器渲染卡牌内容
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 更新进度(按卡牌数量计算)
|
||||||
|
const currentCardIndex = i * totalCards + j + 1;
|
||||||
|
const totalCardCount = totalPages * totalCards;
|
||||||
|
const progress = Math.round((currentCardIndex / totalCardCount) * 100);
|
||||||
|
store.actions.setExportProgress(progress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存 PDF 文件
|
||||||
|
pdf.save('deck.pdf');
|
||||||
|
|
||||||
|
// 完成,关闭预览
|
||||||
|
props.onClose();
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误';
|
||||||
|
store.actions.setExportError(errorMsg);
|
||||||
|
console.error('PDF 导出失败:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存 PDF 文件
|
|
||||||
pdf.save('deck.pdf');
|
|
||||||
|
|
||||||
// 关闭预览
|
|
||||||
props.onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染单个卡片的 SVG 内容(使用 foreignObject)
|
// 渲染单个卡片的 SVG 内容(使用 foreignObject)
|
||||||
|
|
@ -435,6 +455,38 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条和错误信息 */}
|
||||||
|
<Show when={store.state.exportProgress > 0 || store.state.exportError}>
|
||||||
|
<div class="fixed bottom-0 left-0 right-0 z-50 bg-white shadow-lg rounded-lg mx-4 mb-4 px-4 py-3">
|
||||||
|
<Show when={!store.state.exportError}>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-sm text-gray-600">导出进度:</span>
|
||||||
|
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full bg-blue-600 transition-all duration-200"
|
||||||
|
style={{ width: `${store.state.exportProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-gray-700">{store.state.exportProgress}%</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={store.state.exportError}>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-2 text-red-600">
|
||||||
|
<span>❌</span>
|
||||||
|
<span class="text-sm font-medium">{store.state.exportError}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => store.actions.clearExportError()}
|
||||||
|
class="text-gray-500 hover:text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* A4 纸张预览:每页都是一个完整的 SVG */}
|
{/* A4 纸张预览:每页都是一个完整的 SVG */}
|
||||||
<div class="flex flex-col items-center gap-8">
|
<div class="flex flex-col items-center gap-8">
|
||||||
<For each={pages()}>
|
<For each={pages()}>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ export interface DeckState {
|
||||||
|
|
||||||
// 导出状态
|
// 导出状态
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
|
exportProgress: number; // 0-100
|
||||||
|
exportError: string | null;
|
||||||
|
|
||||||
// 打印设置
|
// 打印设置
|
||||||
printOrientation: 'portrait' | 'landscape';
|
printOrientation: 'portrait' | 'landscape';
|
||||||
|
|
@ -106,6 +108,9 @@ export interface DeckActions {
|
||||||
// 导出操作
|
// 导出操作
|
||||||
setExporting: (exporting: boolean) => void;
|
setExporting: (exporting: boolean) => void;
|
||||||
exportDeck: () => void;
|
exportDeck: () => void;
|
||||||
|
setExportProgress: (progress: number) => void;
|
||||||
|
setExportError: (error: string | null) => void;
|
||||||
|
clearExportError: () => void;
|
||||||
|
|
||||||
// 打印设置
|
// 打印设置
|
||||||
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
||||||
|
|
@ -147,6 +152,8 @@ export function createDeckStore(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
|
exportProgress: 0,
|
||||||
|
exportError: null,
|
||||||
printOrientation: 'portrait',
|
printOrientation: 'portrait',
|
||||||
printOddPageOffsetX: 0,
|
printOddPageOffsetX: 0,
|
||||||
printOddPageOffsetY: 0
|
printOddPageOffsetY: 0
|
||||||
|
|
@ -289,9 +296,15 @@ export function createDeckStore(
|
||||||
const setExporting = (exporting: boolean) => setState({ isExporting: exporting });
|
const setExporting = (exporting: boolean) => setState({ isExporting: exporting });
|
||||||
|
|
||||||
const exportDeck = () => {
|
const exportDeck = () => {
|
||||||
setState({ isExporting: true });
|
setState({ isExporting: true, exportProgress: 0, exportError: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setExportProgress = (progress: number) => setState({ exportProgress: progress });
|
||||||
|
|
||||||
|
const setExportError = (error: string | null) => setState({ exportError: error });
|
||||||
|
|
||||||
|
const clearExportError = () => setState({ exportError: null });
|
||||||
|
|
||||||
const setPrintOrientation = (orientation: 'portrait' | 'landscape') => {
|
const setPrintOrientation = (orientation: 'portrait' | 'landscape') => {
|
||||||
setState({ printOrientation: orientation });
|
setState({ printOrientation: orientation });
|
||||||
};
|
};
|
||||||
|
|
@ -332,6 +345,9 @@ export function createDeckStore(
|
||||||
copyCode,
|
copyCode,
|
||||||
setExporting,
|
setExporting,
|
||||||
exportDeck,
|
exportDeck,
|
||||||
|
setExportProgress,
|
||||||
|
setExportError,
|
||||||
|
clearExportError,
|
||||||
setPrintOrientation,
|
setPrintOrientation,
|
||||||
setPrintOddPageOffsetX,
|
setPrintOddPageOffsetX,
|
||||||
setPrintOddPageOffsetY
|
setPrintOddPageOffsetY
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue