Преглед на файлове

fix: refine reading PDF pagination

彭峰 преди 1 месец
родител
ревизия
c4f76f4716

+ 3 - 3
projects/xiaoshu-mobile/src/app/features/english-reading/reading-pdf.component.scss

@@ -2,10 +2,10 @@
 .pdf-page { min-height: 100%; padding-bottom: 76px; }
 .pdf-toolbar { position: sticky; z-index: 4; top: 0; display: grid; min-height: 48px; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 14px; background: white; border-bottom: 1px solid var(--line); }
 .pdf-toolbar button { display: inline-flex; align-items: center; justify-self: start; padding: 7px 8px; color: var(--brand-strong); background: transparent; border: 0; gap: 5px; }.pdf-toolbar strong { font-size: 14px; }.pdf-toolbar > span { justify-self: end; color: var(--ink-soft); font-size: 11px; }
-.pdf-document { width: min(794px, calc(100% - 24px)); margin: 18px auto; padding: 44px 52px; color: #202826; background: white; box-shadow: 0 5px 20px rgb(25 38 34 / 10%); }
+.pdf-document { width: min(794px, calc(100% - 24px)); margin: 18px auto; padding: 44px 52px 0; color: #202826; background: white; box-shadow: 0 5px 20px rgb(25 38 34 / 10%); }
 .reading-section h1 { margin: 0 0 12px; text-align: center; font-family: Georgia, serif; font-size: 28px; line-height: 1.3; letter-spacing: 0; }.document-meta { display: flex; flex-wrap: wrap; justify-content: center; margin-bottom: 24px; color: #68736f; font-size: 11px; gap: 12px; }.reading-content { font-family: Georgia, 'Times New Roman', serif; font-size: 15px; line-height: 1.85; }.reading-content ::ng-deep p { margin: 0 0 12px; }.reading-content ::ng-deep b { color: #1857b8; font-size: 1.2rem; font-weight: 750; }
-.question-section, .answer-section { margin-top: 30px; }.question-section h2, .answer-section h2 { padding-bottom: 8px; font-size: 20px; letter-spacing: 0; border-bottom: 2px solid var(--brand-strong); }.question-item, .answer-item { margin-top: 18px; break-inside: avoid; }.question-item h3, .answer-item h3 { margin: 0 0 10px; font-size: 14px; line-height: 1.45; letter-spacing: 0; }.question-item p { margin: 7px 0 7px 15px; font-size: 13px; }.answer-section { break-before: page; }.answer-item > div { padding: 7px 9px; border-bottom: 1px solid #e6ebe9; }.answer-item p { margin: 0; font-size: 12px; }.answer-item small { display: block; margin-top: 4px; color: #68736f; line-height: 1.5; }.answer-item .correct-answer { background: #edf8f4; }.correct-answer strong { float: right; color: var(--brand-strong); font-size: 10px; }.answer-item footer { margin-top: 9px; padding: 9px; color: #4f5b58; background: #f4f7f6; font-size: 11px; line-height: 1.5; }
+.question-section, .answer-section { margin-top: 0; padding-top: 1px; }.question-section h2, .answer-section h2 { padding-bottom: 8px; font-size: 20px; letter-spacing: 0; border-bottom: 2px solid var(--brand-strong); }.question-item, .answer-item { margin-top: 18px; break-inside: avoid; }.question-item h3, .answer-item h3 { margin: 0 0 10px; font-size: 14px; line-height: 1.45; letter-spacing: 0; }.question-item p { margin: 7px 0 7px 15px; font-size: 13px; }.answer-item > div { padding: 7px 9px; border-bottom: 1px solid #e6ebe9; }.answer-item p { margin: 0; font-size: 12px; }.answer-item small { display: block; margin-top: 4px; color: #68736f; line-height: 1.5; }.answer-item .correct-answer { background: #edf8f4; }.correct-answer strong { float: right; color: var(--brand-strong); font-size: 10px; }.answer-item footer { margin-top: 9px; padding: 9px; color: #4f5b58; background: #f4f7f6; font-size: 11px; line-height: 1.5; }
 .export-footer { position: fixed; z-index: 5; right: 0; bottom: 0; left: 0; display: flex; min-height: 66px; align-items: center; justify-content: center; padding: 10px 16px; background: white; border-top: 1px solid var(--line); }.export-footer button { display: flex; width: min(480px, 100%); align-items: center; justify-content: center; gap: 7px; }.pdf-state { display: flex; min-height: 420px; align-items: center; justify-content: center; color: var(--ink-soft); flex-direction: column; gap: 10px; }.spin { animation: spin .8s linear infinite; }@keyframes spin { to { transform: rotate(360deg); } }
 .wechat-backdrop { position: fixed; z-index: 30; display: grid; inset: 0; padding: 18px; background: rgb(14 25 22 / 62%); place-items: center; }.wechat-dialog { width: min(420px, 100%); padding: 22px; text-align: center; background: white; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }.wechat-dialog h2 { margin: 0 0 10px; font-size: 20px; letter-spacing: 0; }.wechat-dialog p { color: var(--ink-soft); line-height: 1.6; }.wechat-dialog small { display: block; margin: 14px 0; padding: 10px; background: var(--surface-muted); }.wechat-dialog button { width: 100%; }
 @media (max-width: 620px) { .pdf-document { width: 100%; margin: 0; padding: 28px 18px; box-shadow: none; }.reading-section h1 { font-size: 23px; }.pdf-toolbar strong { font-size: 13px; } }
-@media print { .pdf-toolbar, .export-footer { display: none; }.pdf-page { padding: 0; }.pdf-document { width: auto; margin: 0; box-shadow: none; } }
+@media print { .pdf-toolbar, .export-footer { display: none; }.pdf-page { padding: 0; }.pdf-document { width: auto; margin: 0; box-shadow: none; }.question-section, .answer-section { break-before: page; page-break-before: always; } }

+ 22 - 6
projects/xiaoshu-mobile/src/app/features/english-reading/reading-pdf.component.ts

@@ -68,15 +68,31 @@ export class ReadingPdfComponent implements OnInit {
       await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
       await document.fonts?.ready;
       const filename = `${article.title.replace(/[^a-zA-Z0-9一-龥]/g, '_')}_quiz.pdf`;
-      await html2pdf().set({
+      let hasBlankTrailingPage = false;
+      const worker = html2pdf().set({
         margin: [10, 10, 10, 10], filename,
         image: { type: 'jpeg', quality: 1 },
         html2canvas: { scale: 1.5, logging: false, useCORS: true, backgroundColor: '#ffffff' },
         jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
-        pagebreak: { mode: ['css', 'legacy'], before: '.answer-section', avoid: ['.question-item', '.answer-item'] },
-      }).from(element).save();
-    } catch {
-      this.platform.toast('PDF 导出失败,请重试', 'error');
-    } finally { this.exporting.set(false); }
+        pagebreak: {
+          mode: ['css', 'legacy'],
+          before: ['.question-section', '.answer-section'],
+          avoid: ['.question-item', '.answer-item'],
+        },
+      }).from(element).toCanvas().get('canvas', (canvas) => {
+        const pageHeight = Math.floor(canvas.width * (277 / 190));
+        const trailingSlice = canvas.height % pageHeight;
+        hasBlankTrailingPage = trailingSlice > 0 && trailingSlice < 32;
+      }).toPdf();
+      await worker.get('pdf', (pdf) => {
+        const pageCount = pdf.internal.getNumberOfPages();
+        if (hasBlankTrailingPage && pageCount > 1) pdf.deletePage(pageCount);
+      }).save();
+    } catch (error) {
+      const detail = error instanceof Error ? error.message : '';
+      this.platform.toast(detail ? `PDF 导出失败:${detail}` : 'PDF 导出失败,请重试', 'error');
+    } finally {
+      this.exporting.set(false);
+    }
   }
 }

+ 7 - 1
projects/xiaoshu-mobile/src/types/html2pdf.d.ts

@@ -2,8 +2,14 @@ declare module 'html2pdf.js' {
   interface Html2PdfWorker {
     set(options: Record<string, unknown>): Html2PdfWorker;
     from(element: HTMLElement): Html2PdfWorker;
+    toCanvas(): Html2PdfWorker;
+    toPdf(): Html2PdfWorker;
+    get(key: 'canvas', callback: (canvas: HTMLCanvasElement) => void): Html2PdfWorker;
+    get(key: 'pdf', callback: (pdf: {
+      internal: { getNumberOfPages(): number };
+      deletePage(pageNumber: number): void;
+    }) => void): Html2PdfWorker;
     save(): Promise<void>;
   }
   export default function html2pdf(): Html2PdfWorker;
 }
-