chengjie 4 月之前
父节点
当前提交
56afa1b037
共有 2 个文件被更改,包括 221 次插入303 次删除
  1. 二进制
      public/images/PDF.png
  2. 221 303
      src/api/yjbdc/yjbdcController.js

二进制
public/images/PDF.png


+ 221 - 303
src/api/yjbdc/yjbdcController.js

@@ -366,6 +366,7 @@ export async function DeleteYJBDCArticleList(ctx) {
366
 }
366
 }
367
 
367
 
368
 
368
 
369
+
369
 export async function GeneratePDF(ctx) {
370
 export async function GeneratePDF(ctx) {
370
     const params = ctx.request.body;
371
     const params = ctx.request.body;
371
     if (!params || !params.Content) {
372
     if (!params || !params.Content) {
@@ -373,32 +374,32 @@ export async function GeneratePDF(ctx) {
373
         ctx.body = { error: 'Invalid request body: Content is required' };
374
         ctx.body = { error: 'Invalid request body: Content is required' };
374
         return;
375
         return;
375
     }
376
     }
376
-    //文章类型
377
-    const ARTICLE_STYLE={
378
-        "成长":"Personal Growth",
379
-        "童话":"Fairy Tales",
380
-        "家庭亲子":"Family Stories",
381
-        "人生励志":"Inspirational",
382
-        "科幻":"Science Fiction",
383
-        "奇幻":"Fantasy",
384
-        "校园生活":"School Life",
385
-        "节日文化":"Cultural Stories",
386
-        "旅行":"Travel Stories",
387
-        "科普":"Popular Science",
388
-        "动物":"Animal Stories",
389
-        "环保":"Environmental Stories",
377
+
378
+    // 文章类型映射
379
+    const ARTICLE_STYLE = {
380
+        "成长": "Personal Growth",
381
+        "童话": "Fairy Tales",
382
+        "家庭亲子": "Family Stories",
383
+        "人生励志": "Inspirational",
384
+        "科幻": "Science Fiction",
385
+        "奇幻": "Fantasy",
386
+        "校园生活": "School Life",
387
+        "节日文化": "Cultural Stories",
388
+        "旅行": "Travel Stories",
389
+        "科普": "Popular Science",
390
+        "动物": "Animal Stories",
391
+        "环保": "Environmental Stories",
390
     };
392
     };
391
 
393
 
392
-    //等级难度
393
-    const LEVEL=[
394
+    // 等级难度
395
+    const LEVEL = [
394
         "Primary school vocabulary size",
396
         "Primary school vocabulary size",
395
         "Junior high school vocabulary size",
397
         "Junior high school vocabulary size",
396
         "High school vocabulary size",
398
         "High school vocabulary size",
397
         "College vocabulary size"
399
         "College vocabulary size"
398
-    ]
400
+    ];
399
 
401
 
400
     const content = params.Content;
402
     const content = params.Content;
401
-    //console.log("Generating PDF with content:", JSON.stringify(content).substring(0, 200) + "...");
402
     
403
     
403
     try {
404
     try {
404
         // 创建新的 PDF 文档 - 使用A4尺寸
405
         // 创建新的 PDF 文档 - 使用A4尺寸
@@ -416,9 +417,8 @@ export async function GeneratePDF(ctx) {
416
         // 注册中文字体
417
         // 注册中文字体
417
         doc.registerFont('ChineseFont', './public/fonts/方正黑体简体.TTF');
418
         doc.registerFont('ChineseFont', './public/fonts/方正黑体简体.TTF');
418
         
419
         
419
-        // 定义字体选择函数,根据内容是否包含中文选择合适的字体
420
+        // 定义字体选择函数
420
         const selectFont = (text, defaultFont = 'Helvetica') => {
421
         const selectFont = (text, defaultFont = 'Helvetica') => {
421
-            // 检查文本是否包含中文字符
422
             if (/[\u4E00-\u9FFF]/.test(text)) {
422
             if (/[\u4E00-\u9FFF]/.test(text)) {
423
                 return 'ChineseFont';
423
                 return 'ChineseFont';
424
             }
424
             }
@@ -429,302 +429,261 @@ export async function GeneratePDF(ctx) {
429
         const chunks = [];
429
         const chunks = [];
430
         doc.on('data', (chunk) => chunks.push(chunk));
430
         doc.on('data', (chunk) => chunks.push(chunk));
431
         
431
         
432
-        // 像素到点(pt)的转换函数 - A4纸张像素尺寸2100×2970,PDFKit点尺寸595.28×841.89
432
+        // 像素到点(pt)的转换函数
433
         const pixelToPt = (pixel) => pixel * (595.28 / 2100);
433
         const pixelToPt = (pixel) => pixel * (595.28 / 2100);
434
 
434
 
435
-        // 获取文章内容和单词列表
435
+        // 获取文章内容
436
         let articleText = "";
436
         let articleText = "";
437
         if (content.ArticleEnglish) {
437
         if (content.ArticleEnglish) {
438
-            if (Array.isArray(content.ArticleEnglish)) {
439
-                articleText = content.ArticleEnglish.join(" ");
440
-            } else {
441
-                articleText = content.ArticleEnglish;
442
-            }
438
+            articleText = Array.isArray(content.ArticleEnglish) ? 
439
+                content.ArticleEnglish.join(" ") : content.ArticleEnglish;
443
         } else {
440
         } else {
444
             articleText = "No content available";
441
             articleText = "No content available";
445
         }
442
         }
446
-        
447
-        // 清理文章文本中的HTML标签
448
         articleText = articleText.replace(/<[^>]*>/g, '');
443
         articleText = articleText.replace(/<[^>]*>/g, '');
449
             
444
             
450
-        // 获取单词列表(如果存在)
445
+        // 获取单词列表
451
         let words = [];
446
         let words = [];
452
         if (content.Words) {
447
         if (content.Words) {
453
-            try {
454
-                if (typeof content.Words === 'string') {
455
-                    // 尝试解析JSON字符串
456
-                    words = content.Words.split(",");
457
-                } else if (Array.isArray(content.Words)) {
458
-                    // 已经是数组
459
-                    words = content.Words;
460
-                }
461
-            } catch (e) {
462
-                console.error("Error parsing words:", e);
463
-                // 如果解析失败,尝试直接使用
464
-                words = typeof content.Words === 'string' ? content.Words.split(',') : [];
465
-            }
448
+            words = typeof content.Words === 'string' ? 
449
+                content.Words.split(",") : 
450
+                (Array.isArray(content.Words) ? content.Words : []);
466
         }
451
         }
467
-        //console.log("Words to display:", words);
468
 
452
 
469
-        // 获取问题列表(如果存在)
453
+        // 获取问题列表
470
         let questions = [];
454
         let questions = [];
471
         if (content.Question && Array.isArray(content.Question)) {
455
         if (content.Question && Array.isArray(content.Question)) {
472
             questions = content.Question;
456
             questions = content.Question;
473
         }
457
         }
474
-        //console.log("Questions to display:", questions.length);
475
-
476
-        // 1. 在top:90,left:120像素处写"Story",60像素大小,Semibold粗细
477
-        doc.font('Helvetica-Bold')  // 使用Helvetica-Bold作为Semibold替代
478
-           .fontSize(pixelToPt(60))
479
-           .text("Story", pixelToPt(120), pixelToPt(90), {
480
-               width: pixelToPt(200),
481
-               align: 'left'
482
-           });
483
 
458
 
484
-        // 2. 在top:250,left:120像素处写英文文章,48像素大小,宽1240像素,高自适应,Regular粗细
485
-        doc.font('Helvetica')
486
-           .fontSize(pixelToPt(48))
487
-           .text(articleText, pixelToPt(120), pixelToPt(250), {
488
-               width: pixelToPt(1240),
489
-               align: 'left',
490
-               lineGap: pixelToPt(50)  // 行间距
491
-           });
459
+        doc.image('./public/images/PDF.png', 0, 0, {
460
+                width: pixelToPt(2100),
461
+                height: pixelToPt(2970)
462
+        });
492
 
463
 
493
-        // 记录文章结束位置的y坐标
494
-        const articleEndY = doc.y;
495
-        //console.log("Article end Y position:", articleEndY);
464
+        // 1. 标题 - 文章类型
465
+        doc.font(selectFont(ARTICLE_STYLE[content.ArticleStyle] || "Story"))
466
+           .fontSize(pixelToPt(48))
467
+           .text(ARTICLE_STYLE[content.ArticleStyle] || "Story", 
468
+                 pixelToPt(120), pixelToPt(110));
496
 
469
 
497
-        // 3. 在top:90,left:1443像素处写"备注",48像素大小,Regular粗细
498
-        // 使用中文字体显示中文
470
+        // 2. 副标题 - 难度级别
471
+        doc.font('Helvetica')
472
+           .fontSize(pixelToPt(30))
473
+           .text(LEVEL[content.Level] || "", 
474
+                 pixelToPt(120), pixelToPt(170));
475
+
476
+        // 3. 时间
477
+        const currentDate = moment().format("YYYY年MM月DD日 HH:mm");
478
+        if (params.CreateTime)
479
+            currentDate=moment(params.CreateTime).format("YYYY年MM月DD日 HH:mm");
499
         doc.font('ChineseFont')
480
         doc.font('ChineseFont')
500
-           .fontSize(pixelToPt(48))
501
-           .text("备注", pixelToPt(1443), pixelToPt(90), {
502
-               width: pixelToPt(537),
503
-               align: 'left'
504
-           });
481
+           .fontSize(pixelToPt(30))
482
+           .text(currentDate, 
483
+                 pixelToPt(120), pixelToPt(212));
484
+
505
 
485
 
506
-        // 4. 在top:210,left:1443像素处画一条黑线,537像素宽,10像素高
507
-        doc.rect(pixelToPt(1443), pixelToPt(210), pixelToPt(537), pixelToPt(10))
486
+        // 4. 黑线
487
+        doc.rect(pixelToPt(120), pixelToPt(289), pixelToPt(537), pixelToPt(10))
508
            .fill('black');
488
            .fill('black');
509
 
489
 
510
-        // 5. 在top:250,left:1443像素,宽537像素,高900像素的空间内竖排显示1-10个单词,每行一个,右对齐
511
-        doc.font('Helvetica');  // 使用常规体(Regular)
512
-           
513
-        // 计算单词显示区域
514
-        const wordAreaTop = pixelToPt(250);
515
-        const wordAreaLeft = pixelToPt(1443);
516
-        const wordAreaWidth = pixelToPt(537);
517
-        const wordAreaHeight = pixelToPt(900);
518
-        
519
-        // 确保words是数组
520
-        const wordsArray = Array.isArray(words) ? words : 
521
-                          (typeof words === 'string' ? words.split(',') : []);
522
-        
523
-        // 显示单词(最多10个)
524
-        const maxWords = Math.min(wordsArray.length, 10);
525
-        
526
-        // 根据单词数量动态计算每个单词的高度空间
527
-        const wordHeight = pixelToPt(80);  // 固定高度,确保足够的空间显示
528
-        
529
-        // 首先确定所有单词中需要的最小字体大小
530
-        let minFontSize = pixelToPt(48); // 默认字体大小
531
-        for (let i = 0; i < maxWords; i++) {
532
-            const word = wordsArray[i];
533
-            if (word) {
534
-                const wordLength = word.toString().length;
535
-                // 根据词组长度确定需要的字体大小
536
-                if (wordLength > 15) {
537
-                    minFontSize = Math.min(minFontSize, pixelToPt(36)); // 长词组使用较小字体
538
-                } else if (wordLength > 10) {
539
-                    minFontSize = Math.min(minFontSize, pixelToPt(42)); // 中等长度词组使用中等字体
540
-                }
541
-            }
542
-        }
543
-        
544
-        // 使用确定的字体大小显示所有单词
545
-        doc.fontSize(minFontSize);
546
-        console.log(`Using font size ${minFontSize} for all words`);
490
+        // 5. 单词列表
491
+        doc.font('Helvetica')
492
+           .fontSize(pixelToPt(36));
547
         
493
         
548
-        for (let i = 0; i < maxWords; i++) {
549
-            const word = wordsArray[i];
550
-            if (word) {  // 确保单词存在
551
-                const yPosition = wordAreaTop + (i * wordHeight);
552
-                
553
-                // 确保不超出指定区域
554
-                if (yPosition + wordHeight <= wordAreaTop + wordAreaHeight) {
555
-                    // 添加调试信息
556
-                    console.log(`Drawing word: "${word}" at position: ${yPosition}`);
557
-                    
558
-                    doc.text(word.toString(), wordAreaLeft, yPosition, {
559
-                        width: wordAreaWidth,
560
-                        align: 'right',  // 右对齐
561
-                        lineBreak: true  // 允许自动换行,处理特别长的词组
562
-                    });
563
-                }
564
-            }
565
-        }
494
+        let wordY = pixelToPt(364);
495
+        words.slice(0, 10).forEach(word => {
496
+            doc.text(word, pixelToPt(122), wordY, {
497
+                width: pixelToPt(535),
498
+                align: 'left'
499
+            });
500
+            wordY += pixelToPt(70);
501
+        });
502
+
503
+        // 6. 文章内容
504
+        doc.font('Helvetica')
505
+           .fontSize(pixelToPt(48))
506
+           .text(articleText, pixelToPt(740), pixelToPt(105), {
507
+               width: pixelToPt(1240),
508
+               lineGap: pixelToPt(40.5)
509
+           });
566
 
510
 
567
-        // 6. 在文章显示全部完成的下方100像素,left是120位置,画一个黑线,1860像素宽,10像素高
568
-        const lineY = Math.max(articleEndY, wordAreaTop + wordAreaHeight) + pixelToPt(100);
569
-        doc.rect(pixelToPt(120), lineY, pixelToPt(1860), pixelToPt(10))
511
+        // 7. 黑线
512
+        const articleEndY = doc.y;
513
+        doc.rect(pixelToPt(120), articleEndY + pixelToPt(41), 
514
+                pixelToPt(1860), pixelToPt(10))
570
            .fill('black');
515
            .fill('black');
571
 
516
 
572
-        // 7-12. 添加问题和答案部分
517
+        // 8-13. 问题和答案
573
         if (questions.length > 0) {
518
         if (questions.length > 0) {
574
-            // 问题1和答案 - left:120像素位置
575
-            if (questions.length >= 1) {
576
-                // 问题标题 - 检查是否包含中文并使用适当的字体
577
-                const q1Text = `1. ${questions[0].QuestionEnglish || "Question 1"}`;
578
-                doc.font(/[\u4E00-\u9FFF]/.test(q1Text) ? 'ChineseFont' : 'Helvetica-Bold')
579
-                   .fontSize(pixelToPt(36))
580
-                   .text(q1Text, pixelToPt(120), lineY + pixelToPt(100), {
581
-                       width: pixelToPt(540),
582
-                       align: 'left'
583
-                   });
584
-                
585
-                // 问题选项 - 使用常规字体(Regular)
586
-                doc.font('Helvetica') // 明确设置为常规字体,不使用粗体
587
-                   .fontSize(pixelToPt(36));
588
-                
589
-                const options = questions[0].OptionsEnglish || [];
590
-                let optionY = doc.y + pixelToPt(20);
591
-                
592
-                for (let i = 0; i < options.length; i++) {
593
-                    doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, {
594
-                        width: pixelToPt(540),
595
-                        align: 'left'
519
+            // 定义问题的X坐标位置
520
+            const questionXPositions = [
521
+                pixelToPt(120),  // 问题1
522
+                pixelToPt(120),  // 问题2
523
+                pixelToPt(740),  // 问题3
524
+                pixelToPt(740),  // 问题4
525
+                pixelToPt(1360)  // 问题5
526
+            ];
527
+            
528
+            // 存储每列最后一个选项的Y坐标位置
529
+            let lastOptionYPositions = {
530
+                column1: 0, // 用于跟踪第一列(问题1)的最后位置
531
+                column2: 0, // 用于跟踪第二列(问题3)的最后位置
532
+            };
533
+            
534
+            // 首先渲染问题1、3、5(第一行问题)
535
+            const firstRowQuestions = [0, 2, 4]; // 问题1、3、5的索引
536
+            
537
+            for (const i of firstRowQuestions) {
538
+                if (i < questions.length && questions[i]) {
539
+                    const currentX = questionXPositions[i];
540
+                    const currentY = articleEndY + pixelToPt(130); // 所有第一行问题的起始Y坐标相同
541
+                    
542
+                    // 渲染问题文本
543
+                    doc.font('Helvetica-Bold')
544
+                       .fontSize(pixelToPt(36))
545
+                       .text(`${i+1}. ${questions[i].QuestionEnglish || `Question ${i+1}`}`, 
546
+                             currentX, currentY, {
547
+                           width: pixelToPt(540),
548
+                           align: 'left'
549
+                       });
550
+                    
551
+                    // 获取问题文本渲染后的Y坐标位置
552
+                    const questionEndY = doc.y;
553
+                    
554
+                    // 选项起始位置 = 问题结束位置 + 20像素间距
555
+                    let optionY = questionEndY + pixelToPt(28);
556
+                    
557
+                    // 渲染选项
558
+                    const options = questions[i].OptionsEnglish || [];
559
+                    options.forEach((opt) => {
560
+                        doc.font('Helvetica')
561
+                           .fontSize(pixelToPt(36))
562
+                           .text(`${opt}`, currentX, optionY);
563
+                        
564
+                        // 更新选项Y坐标,为下一个选项做准备
565
+                        optionY = doc.y + pixelToPt(8);
596
                     });
566
                     });
597
-                    optionY = doc.y + pixelToPt(10);
567
+                    
568
+                    // 保存该列最后一个选项的Y坐标
569
+                    if (i === 0) {
570
+                        lastOptionYPositions.column1 = doc.y;
571
+                    } else if (i === 2) {
572
+                        lastOptionYPositions.column2 = doc.y;
573
+                    }
598
                 }
574
                 }
599
             }
575
             }
600
             
576
             
601
-            // 问题2和答案 - left:120像素位置
602
-            if (questions.length >= 2) {
603
-                // 获取问题1结束的Y坐标
604
-                const q1EndY = doc.y + pixelToPt(60);
577
+            // 然后渲染问题2和4(第二行问题)
578
+            if (questions.length > 1 && questions[1]) {
579
+                // 问题2位于问题1的选项下方60像素处
580
+                const question2Y = lastOptionYPositions.column1 + pixelToPt(75);
605
                 
581
                 
606
-                // 问题标题
607
                 doc.font('Helvetica-Bold')
582
                 doc.font('Helvetica-Bold')
608
                    .fontSize(pixelToPt(36))
583
                    .fontSize(pixelToPt(36))
609
-                   .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`, pixelToPt(120), q1EndY, {
584
+                   .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`, 
585
+                         questionXPositions[1], question2Y, {
610
                        width: pixelToPt(540),
586
                        width: pixelToPt(540),
611
                        align: 'left'
587
                        align: 'left'
612
                    });
588
                    });
613
                 
589
                 
614
-                // 问题选项
615
-                doc.font('Helvetica')
616
-                   .fontSize(pixelToPt(36));
590
+                // 获取问题文本渲染后的Y坐标位置
591
+                const questionEndY = doc.y;
617
                 
592
                 
618
-                const options = questions[1].OptionsEnglish || [];
619
-                let optionY = doc.y + pixelToPt(20);
593
+                // 选项起始位置 = 问题结束位置 + 20像素间距
594
+                let optionY = questionEndY + pixelToPt(28);
620
                 
595
                 
621
-                for (let i = 0; i < options.length; i++) {
622
-                    doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, {
623
-                        width: pixelToPt(540),
624
-                        align: 'left'
625
-                    });
626
-                    optionY = doc.y + pixelToPt(10);
627
-                }
596
+                // 渲染选项
597
+                const options = questions[1].OptionsEnglish || [];
598
+                options.forEach((opt) => {
599
+                    doc.font('Helvetica')
600
+                       .fontSize(pixelToPt(36))
601
+                       .text(`${opt}`, questionXPositions[1], optionY);
602
+                    
603
+                    // 更新选项Y坐标,为下一个选项做准备
604
+                    optionY = doc.y + pixelToPt(8);
605
+                });
628
             }
606
             }
629
             
607
             
630
-            // 问题3和问题4 - left:740像素位置
631
-            if (questions.length >= 3) {
632
-                // 问题3标题
608
+            if (questions.length > 3 && questions[3]) {
609
+                // 问题4位于问题3的选项下方60像素处
610
+                const question4Y = lastOptionYPositions.column2 + pixelToPt(75);
611
+                
633
                 doc.font('Helvetica-Bold')
612
                 doc.font('Helvetica-Bold')
634
                    .fontSize(pixelToPt(36))
613
                    .fontSize(pixelToPt(36))
635
-                   .text(`3. ${questions[2].QuestionEnglish || "Question 3"}`, pixelToPt(740), lineY + pixelToPt(100), {
614
+                   .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`, 
615
+                         questionXPositions[3], question4Y, {
636
                        width: pixelToPt(540),
616
                        width: pixelToPt(540),
637
                        align: 'left'
617
                        align: 'left'
638
                    });
618
                    });
639
                 
619
                 
640
-                // 问题3选项
641
-                doc.font('Helvetica')
642
-                   .fontSize(pixelToPt(36));
620
+                // 获取问题文本渲染后的Y坐标位置
621
+                const questionEndY = doc.y;
643
                 
622
                 
644
-                const options3 = questions[2].OptionsEnglish || [];
645
-                let option3Y = doc.y + pixelToPt(20);
646
-                
647
-                for (let i = 0; i < options3.length; i++) {
648
-                    doc.text(options3[i] || `Option ${i+1}`, pixelToPt(740), option3Y, {
649
-                        width: pixelToPt(540),
650
-                        align: 'left'
651
-                    });
652
-                    option3Y = doc.y + pixelToPt(10);
653
-                }
623
+                // 选项起始位置 = 问题结束位置 + 20像素间距
624
+                let optionY = questionEndY + pixelToPt(28);
654
                 
625
                 
655
-                // 问题4 - 如果存在
656
-                if (questions.length >= 4) {
657
-                    // 获取问题3结束的Y坐标
658
-                    const q3EndY = doc.y + pixelToPt(60);
659
-                    
660
-                    // 问题4标题
661
-                    doc.font('Helvetica-Bold')
662
-                       .fontSize(pixelToPt(36))
663
-                       .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`, pixelToPt(740), q3EndY, {
664
-                           width: pixelToPt(540),
665
-                           align: 'left'
666
-                       });
667
-                    
668
-                    // 问题4选项
626
+                // 渲染选项
627
+                const options = questions[3].OptionsEnglish || [];
628
+                options.forEach((opt) => {
669
                     doc.font('Helvetica')
629
                     doc.font('Helvetica')
670
-                       .fontSize(pixelToPt(36));
671
-                    
672
-                    const options4 = questions[3].OptionsEnglish || [];
673
-                    let option4Y = doc.y + pixelToPt(20);
630
+                       .fontSize(pixelToPt(36))
631
+                       .text(`${opt}`, questionXPositions[3], optionY);
674
                     
632
                     
675
-                    for (let i = 0; i < options4.length; i++) {
676
-                        doc.text(options4[i] || `Option ${i+1}`, pixelToPt(740), option4Y, {
677
-                            width: pixelToPt(540),
678
-                            align: 'left'
679
-                        });
680
-                        option4Y = doc.y + pixelToPt(10);
681
-                    }
682
-                }
633
+                    // 更新选项Y坐标,为下一个选项做准备
634
+                    optionY = doc.y + pixelToPt(8);
635
+                });
683
             }
636
             }
684
             
637
             
685
-            // 问题5 - left:1360像素位置
686
-            if (questions.length >= 5) {
687
-                // 问题5标题
638
+            // 问题5保持原位置不变
639
+            if (questions.length > 4 && questions[4]) {
640
+                const currentX = questionXPositions[4];
641
+                const currentY = articleEndY + pixelToPt(130);
642
+                
688
                 doc.font('Helvetica-Bold')
643
                 doc.font('Helvetica-Bold')
689
                    .fontSize(pixelToPt(36))
644
                    .fontSize(pixelToPt(36))
690
-                   .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`, pixelToPt(1360), lineY + pixelToPt(100), {
645
+                   .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`, 
646
+                         currentX, currentY, {
691
                        width: pixelToPt(540),
647
                        width: pixelToPt(540),
692
                        align: 'left'
648
                        align: 'left'
693
                    });
649
                    });
694
                 
650
                 
695
-                // 问题5选项
696
-                doc.font('Helvetica')
697
-                   .fontSize(pixelToPt(36));
651
+                const questionEndY = doc.y;
698
                 
652
                 
699
-                const options5 = questions[4].OptionsEnglish || [];
700
-                let option5Y = doc.y + pixelToPt(20);
653
+                let optionY = questionEndY + pixelToPt(28);
701
                 
654
                 
702
-                for (let i = 0; i < options5.length; i++) {
703
-                    doc.text(options5[i] || `Option ${i+1}`, pixelToPt(1360), option5Y, {
704
-                        width: pixelToPt(540),
705
-                        align: 'left'
706
-                    });
707
-                    option5Y = doc.y + pixelToPt(10);
708
-                }
655
+                // 渲染选项
656
+                const options = questions[4].OptionsEnglish || [];
657
+                options.forEach((opt) => {
658
+                    doc.font('Helvetica')
659
+                       .fontSize(pixelToPt(36))
660
+                       .text(`${opt}`, currentX, optionY);
661
+                    
662
+                    // 更新选项Y坐标,为下一个选项做准备
663
+                    optionY = doc.y + pixelToPt(8);
664
+                });
709
             }
665
             }
710
         }
666
         }
711
 
667
 
712
-        // 13. 下方距离底边330像素,left:120像素处,画一条虚线,1860像素宽,20像素高,颜色为#D2D2D2
713
-        const dashLineY = doc.page.height - pixelToPt(330);
668
+        // 14. 虚线
669
+        const lastContentY = doc.page.height - pixelToPt(190);
670
+        // doc.rect(pixelToPt(120), lastContentY, 
671
+        //         pixelToPt(1630), pixelToPt(10))
672
+        //    .fill('#D2D2D2');
714
         doc.strokeColor('#D2D2D2')
673
         doc.strokeColor('#D2D2D2')
715
-           .dash(pixelToPt(20), { space: pixelToPt(20) }) // 设置虚线样式:宽20像素,间隔20像素
674
+           .dash(pixelToPt(20), { space: pixelToPt(28) }) // 设置虚线样式:宽20像素,间隔20像素
716
            .lineWidth(pixelToPt(10)) // 线高10像素
675
            .lineWidth(pixelToPt(10)) // 线高10像素
717
-           .moveTo(pixelToPt(120), dashLineY)
718
-           .lineTo(pixelToPt(120) + pixelToPt(1860), dashLineY)
676
+           .moveTo(pixelToPt(120), lastContentY)
677
+           .lineTo(pixelToPt(120) + pixelToPt(1630), lastContentY)
719
            .stroke()
678
            .stroke()
720
            .undash(); // 重置虚线样式
679
            .undash(); // 重置虚线样式
721
 
680
 
722
-        // 14-18. 显示问题答案
681
+        // 15-19. 答案和底部信息
723
         doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代
682
         doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代
724
-           .fontSize(pixelToPt(45))
683
+           .fontSize(pixelToPt(36))
725
            .fillColor('black'); // 重置颜色为黑色
684
            .fillColor('black'); // 重置颜色为黑色
726
         
685
         
727
-        const answersY = doc.page.height - pixelToPt(177);
686
+        const answersY = doc.page.height - pixelToPt(144);
728
         
687
         
729
         // 获取问题答案
688
         // 获取问题答案
730
         const answers = [];
689
         const answers = [];
@@ -736,42 +695,19 @@ export async function GeneratePDF(ctx) {
736
         }
695
         }
737
         
696
         
738
         // 显示答案(如果存在)
697
         // 显示答案(如果存在)
739
-        const answerPositions = [120, 277, 443, 611, 778];
698
+        const answerPositions = [120, 262, 411, 561, 711];
740
         for (let i = 0; i < Math.min(answers.length, 5); i++) {
699
         for (let i = 0; i < Math.min(answers.length, 5); i++) {
741
-            doc.text(answers[i], pixelToPt(answerPositions[i]), answersY, {
700
+            doc.text(answers[i], pixelToPt(answerPositions[i]), answersY+2, {
742
                 width: pixelToPt(100),
701
                 width: pixelToPt(100),
743
                 align: 'left'
702
                 align: 'left'
744
             });
703
             });
745
         }
704
         }
746
-        
747
-        // 20. 显示"秒过·语境背单词"
748
-        const appNameY = doc.page.height - pixelToPt(223);
749
-        doc.font('ChineseFont')  // 使用中文字体显示中文
750
-           .fontSize(pixelToPt(48))
751
-           .fillColor('black')
752
-           .text("秒过·语境背单词", pixelToPt(1320), appNameY, {
753
-               width: pixelToPt(400),
754
-               align: 'right'
755
-           });
756
-        
757
-        // 21. 显示"微信小程序"
758
-        const appTypeY = doc.page.height - pixelToPt(163);
759
-        doc.font('ChineseFont')  // 使用中文字体显示中文
760
-           .fontSize(pixelToPt(28))
761
-           .text("微信小程序", pixelToPt(1320), appTypeY, {
762
-               width: pixelToPt(400),
763
-               align: 'right'
764
-           });
765
 
705
 
766
-        // 19. 显示当前时间
767
-        const currentTime = moment().format('YYYY年MM月DD日 HH:mm');
768
-        const timeY = doc.page.height - pixelToPt(118);
769
-        // 计算正确的位置:总宽度2100像素 - 右边距380像素 = 右边缘位置1720像素
770
-        // 为了使用text()方法的右对齐,设置起始位置为1720-380=1340像素
771
-        doc.font('ChineseFont')  // 使用中文字体显示包含中文的日期
772
-           .fontSize(pixelToPt(32))
773
-           .text(currentTime, pixelToPt(1340), timeY, {
774
-               width: pixelToPt(380),
706
+        // 20-21. 应用名称
707
+        doc.font('ChineseFont')
708
+           .fontSize(pixelToPt(36))
709
+           .text("语境背单词(微信小程序)", pixelToPt(1338), answersY,{
710
+               width: pixelToPt(440),
775
                align: 'right'
711
                align: 'right'
776
            });
712
            });
777
         
713
         
@@ -782,7 +718,7 @@ export async function GeneratePDF(ctx) {
782
             const qrCodeWidth = pixelToPt(200);
718
             const qrCodeWidth = pixelToPt(200);
783
             const qrCodeHeight = pixelToPt(200);
719
             const qrCodeHeight = pixelToPt(200);
784
             const qrCodeX = doc.page.width - pixelToPt(120) - qrCodeWidth; // 右边距离转换为左边距离
720
             const qrCodeX = doc.page.width - pixelToPt(120) - qrCodeWidth; // 右边距离转换为左边距离
785
-            const qrCodeY = doc.page.height - pixelToPt(60) - qrCodeHeight; // 底部距离转换为顶部距离
721
+            const qrCodeY = doc.page.height - pixelToPt(100) - qrCodeHeight; // 底部距离转换为顶部距离
786
             
722
             
787
             // 添加二维码图片
723
             // 添加二维码图片
788
             doc.image('./public/images/acode/YJBDC_QRCode.png', qrCodeX, qrCodeY, {
724
             doc.image('./public/images/acode/YJBDC_QRCode.png', qrCodeX, qrCodeY, {
@@ -794,44 +730,26 @@ export async function GeneratePDF(ctx) {
794
             console.error("Error adding QR Code image:", imgError);
730
             console.error("Error adding QR Code image:", imgError);
795
         }
731
         }
796
         
732
         
797
-        //console.log("PDF generation completed, finalizing document...");
798
 
733
 
799
-        // 使用 Promise 等待 PDF 生成完成
800
-        await new Promise((resolve, reject) => {
801
-            // 监听 end 事件,表示 PDF 生成完成
802
-            doc.on('end', () => {
803
-                try {
804
-                    // 将所有数据块合并为一个 Buffer
805
-                    const pdfBuffer = Buffer.concat(chunks);
806
-                    console.log("PDF buffer size:", pdfBuffer.length);
807
-                    
808
-                    // 设置响应头
809
-                    ctx.set('Content-Type', 'application/pdf');
810
-                    ctx.set('Content-Disposition', 'attachment; filename=reading.pdf');
811
-                    
812
-                    // 设置响应体为 PDF buffer
813
-                    ctx.body = pdfBuffer;
814
-                    resolve();
815
-                } catch (err) {
816
-                    console.error("Error in PDF end handler:", err);
817
-                    reject(err);
818
-                }
819
-            });
734
+        // 结束PDF生成
735
+        doc.end();
820
 
736
 
821
-            // 监听错误事件
822
-            doc.on('error', (err) => {
823
-                console.error("PDF generation error:", err);
824
-                reject(err);
737
+        // 等待PDF生成完成
738
+        const pdfBuffer = await new Promise((resolve) => {
739
+            doc.on('end', () => {
740
+                resolve(Buffer.concat(chunks));
825
             });
741
             });
826
-
827
-            // 结束文档生成
828
-            doc.end();
829
         });
742
         });
830
 
743
 
744
+        // 设置响应头
745
+        ctx.set('Content-Type', 'application/pdf');
746
+        ctx.set('Content-Disposition', 'attachment; filename=article.pdf');
747
+        ctx.body = pdfBuffer;
748
+
831
     } catch (error) {
749
     } catch (error) {
832
-        console.error('Error generating PDF:', error);
750
+        console.error("Error generating PDF:", error);
833
         ctx.status = 500;
751
         ctx.status = 500;
834
-        ctx.body = { error: error.message, stack: error.stack };
752
+        ctx.body = { error: "Failed to generate PDF" };
835
     }
753
     }
836
 }
754
 }
837
 
755