Sfoglia il codice sorgente

得分公式修复和知识点讲解功能添加(8成熟)

yemeishu 2 settimane fa
parent
commit
4f14f3eabe

+ 15 - 14
app/Services/ExamPdfExportService.php

@@ -2474,43 +2474,44 @@ class ExamPdfExportService
     private function getDefaultExplanation(string $kpCode, string $kpName): string
     {
         // 默认讲解模板
-        return <<<MARKDOWN
+        // 注意:PHP HEREDOC 中 \e 会被解释为 ESC 字符,所以需要用 \\end 而非 \end
+        return <<<'MARKDOWN'
 ## 知识点
-(1) 核心定义:二元一次方程是含有两个未知数且每个未知数的次数都是 1 的方程,一般形式为 \$ax + by = c\$(其中 \$a,b,c\$ 为常数,且 \$a,b\$ 不同时为 0)。二元一次方程组是由两个(或两个以上)二元一次方程组成,常见为
-\$\$
+(1) 核心定义:二元一次方程是含有两个未知数且每个未知数的次数都是 1 的方程,一般形式为 $ax + by = c$(其中 $a,b,c$ 为常数,且 $a,b$ 不同时为 0)。二元一次方程组是由两个(或两个以上)二元一次方程组成,常见为
+$$
 \begin{cases}
 a_1x + b_1y = c_1\\
 a_2x + b_2y = c_2
 \end{cases}
-\$\$
+$$
 方程组应用建模就是把题目中的数量关系翻译成方程组,通过求解得到实际问题的答案。
 
-(2) 性质定理:方程组的解必须同时满足方程组中的每一个方程,因此解是两个方程公共的 \$(x,y)\$。常用解法有代入消元法与加减消元法:代入消元法适合某个方程易表示成 \$x = \dots\$ 或 \$y = \dots\$;加减消元法适合两个方程中某个未知数的系数相同或相反,便于相加或相减消去一个未知数。建模时常用关系包括:总量关系(如"总数 = 部分之和")、单价数量总价关系(\$总价 = 单价 \times 数量\$)、路程速度时间关系(\$路程 = 速度 \times 时间\$)等。
+(2) 性质定理:方程组的解必须同时满足方程组中的每一个方程,因此解是两个方程公共的 $(x,y)$。常用解法有代入消元法与加减消元法:代入消元法适合某个方程易表示成 $x = \dots$ 或 $y = \dots$;加减消元法适合两个方程中某个未知数的系数相同或相反,便于相加或相减消去一个未知数。建模时常用关系包括:总量关系(如“总数 = 部分之和”)、单价数量总价关系(“$总价 = 单价 \times 数量$”)、路程速度时间关系($路程 = 速度 \times 时间$)等。
 
-(3) 注意事项:设未知数要带单位与含义,例如"设甲买了 \$x\$ 支笔";列方程要对应同一类量,别把"支数"和"元数"混在一条等式里;检查条件是否满足题意(如数量应为整数且 \$> 0\$);消元后别忘了回代求另一个未知数;最后答案要写清对象与单位,并可把解代回原式检验。
+(3) 注意事项:设未知数要带单位与含义,例如“设甲买了 $x$ 支笔”;列方程要对应同一类量,别把“支数”和“元数”混在一条等式里;检查条件是否满足题意(如数量应为整数且 $> 0$);消元后别忘了回代求另一个未知数;最后答案要写清对象与单位,并可把解代回原式检验。
 
 ## 知识点应用
 - 典型例题:文具店买笔和本子。小明买了 2 支笔和 3 本本子共花 19 元;小红买了 3 支笔和 2 本本子共花 18 元。求每支笔和每本本子的单价。
 
 - 关键步骤:
-  1. 设未知数并写出含义:设每支笔单价为 \$x\$ 元,每本本子单价为 \$y\$ 元。
-  2. 根据题意列方程组:由"总价 = 单价 \times 数量"
-     \$\$
+  1. 设未知数并写出含义:设每支笔单价为 $x$ 元,每本本子单价为 $y$ 元。
+  2. 根据题意列方程组:由“$总价 = 单价 \times 数量$”
+     $$
      \begin{cases}
      2x + 3y = 19\\
      3x + 2y = 18
      \end{cases}
-     \$\$
+     $$
   3. 选择消元并求解:用加减消元。将第一式乘 3、第二式乘 2:
-     \$\$
+     $$
      \begin{cases}
      6x + 9y = 57\\
      6x + 4y = 36
      \end{cases}
-     \$\$
-     相减得 \$5y = 21\$,所以 \$y = \dfrac{21}{5} = 4.2\$。代入 \$3x + 2y = 18\$ 得 \$3x + 8.4 = 18\$,所以 \$3x = 9.6\$,\$x = 3.2\$。
+     $$
+     相减得 $5y = 21$,所以 $y = \dfrac{21}{5} = 4.2$。代入 $3x + 2y = 18$ 得 $3x + 8.4 = 18$,所以 $3x = 9.6$,$x = 3.2$。
 
-- 结论:每支笔 3.2 元,每本本子 4.2 元(两者均 \$> 0\$,符合题意)。
+- 结论:每支笔 3.2 元,每本本子 4.2 元(两者均 $> 0$,符合题意)。
 MARKDOWN;
     }
 }

+ 112 - 60
resources/views/pdf/exam-knowledge-explanation.blade.php

@@ -1,4 +1,4 @@
-{{-- 知识点讲解完整模板 --}}
+{{-- 知识点讲解模板 --}}
 <!DOCTYPE html>
 <html lang="zh-CN">
 <head>
@@ -6,17 +6,14 @@
     <title>知识点讲解</title>
     <link rel="stylesheet" href="/css/katex/katex.min.css">
     @include('pdf.partials.kp-explain-styles')
-    @include('pdf.partials.katex-scripts')
-    <script src="/js/math-render.js"></script>
 </head>
 <body>
-    <div class="kp-explain">
+    <div class="page">
         <div class="kp-explain-header">
             <div class="kp-explain-title">知识点讲解</div>
-            <div class="kp-explain-subtitle">
-                本章节用于梳理本卷涉及的知识点,帮助学生在做题前完成预习/复盘。
-            </div>
+            <div class="kp-explain-subtitle">本章节用于梳理本卷涉及的知识点,帮助学生在做题前完成预习/复盘。</div>
         </div>
+
         @if(empty($knowledgePoints))
             <div class="kp-empty">暂无知识点数据</div>
         @else
@@ -28,31 +25,10 @@
                         </div>
                         <div class="kp-section-body">
                             @if(!empty($kp['explanation']))
-                                {{-- 解析 ## 标题分段:## 知识点 和 ## 知识点应用 --}}
-                                @php
-                                    $explanation = $kp['explanation'];
-                                    // 提取 ## 知识点 部分
-                                    preg_match('/##\s*知识点\s*\n([\s\S]*?)(?=##\s*知识点应用|$)/u', $explanation, $knowledgeMatch);
-                                    $knowledgeContent = isset($knowledgeMatch[1]) ? trim($knowledgeMatch[1]) : '';
-
-                                    // 提取 ## 知识点应用 部分
-                                    preg_match('/##\s*知识点应用\s*\n([\s\S]*)/u', $explanation, $applicationMatch);
-                                    $applicationContent = isset($applicationMatch[1]) ? trim($applicationMatch[1]) : '';
-                                @endphp
-
-                                @if(!empty($knowledgeContent))
-                                    <div class="kp-block">
-                                        <div class="kp-block-title"><span class="check">✓</span>知识点</div>
-                                        <div class="kp-markdown-content">{!! \App\Services\MathFormulaProcessor::processFormulas($knowledgeContent) !!}</div>
-                                    </div>
-                                @endif
-
-                                @if(!empty($applicationContent))
-                                    <div class="kp-block">
-                                        <div class="kp-block-title"><span class="check">✓</span>知识点应用</div>
-                                        <div class="kp-markdown-content">{!! \App\Services\MathFormulaProcessor::processFormulas($applicationContent) !!}</div>
-                                    </div>
-                                @endif
+                                {{-- 隐藏容器存储原始 Markdown --}}
+                                <div class="kp-markdown-source" style="display:none;">{!! $kp['explanation'] !!}</div>
+                                {{-- 渲染容器 --}}
+                                <div class="kp-markdown-container kp-markdown-content"></div>
                             @endif
                         </div>
                     </div>
@@ -60,40 +36,116 @@
             </div>
         @endif
     </div>
+
+    {{-- 引入脚本 --}}
+    <script src="/js/markdown-it.min.js"></script>
+    <script src="/js/katex.min.js"></script>
+
     <script>
-        // 确保数学公式渲染系统已初始化
-        document.addEventListener('DOMContentLoaded', function() {
-            // 延迟渲染,确保所有脚本加载完成
-            setTimeout(function() {
-                // 渲染 kp-markdown-content 中的公式
-                document.querySelectorAll('.kp-markdown-content').forEach(function(el) {
-                    if (typeof window.renderMathElement === 'function' && el.dataset.rendered !== 'true') {
-                        window.renderMathElement(el);
-                    }
-                });
-                
-                // 如果 MathRender 系统可用,使用它
-                if (typeof window.MathRender !== 'undefined') {
-                    // 临时修改 selector 以渲染 kp-markdown-content
-                    var originalSelector = window.MathRenderConfig.selector;
-                    window.MathRenderConfig.selector = '.kp-markdown-content';
-                    window.MathRender.renderAll();
-                    window.MathRenderConfig.selector = originalSelector;
+    (function() {
+        'use strict';
+
+        function waitForLibs(callback) {
+            let attempts = 0;
+            const maxAttempts = 50;
+            const interval = setInterval(function() {
+                attempts++;
+                if (typeof window.markdownit === 'function') {
+                    clearInterval(interval);
+                    callback();
+                } else if (attempts >= maxAttempts) {
+                    clearInterval(interval);
+                    console.error('[Render] Libraries failed to load after', maxAttempts, 'attempts');
+                    console.log('[Render] markdownit:', typeof window.markdownit);
+                    callback();
                 }
-                
-                // 触发自定义事件,让其他渲染系统也能响应
-                document.dispatchEvent(new CustomEvent('math:render'));
             }, 100);
-            
-            // 再次尝试渲染,处理可能的异步加载情况
-            setTimeout(function() {
-                document.querySelectorAll('.kp-markdown-content').forEach(function(el) {
-                    if (typeof window.renderMathElement === 'function' && el.dataset.rendered !== 'true') {
-                        window.renderMathElement(el);
+        }
+
+        function renderMarkdown(md, targetEl) {
+            if (!md) return;
+
+            if (typeof window.markdownit !== 'function') {
+                targetEl.textContent = md;
+                return;
+            }
+
+            const mdParser = window.markdownit({
+                html: false,
+                breaks: false,
+                linkify: true,
+                typographer: false
+            });
+
+            let html = mdParser.render(md);
+
+            if (typeof window.katex !== 'undefined') {
+                const katexOptions = {
+                    throwOnError: false,
+                    displayMode: false
+                };
+
+                function decodeEntities(input) {
+                    return input
+                        .replace(/&gt;/g, '>')
+                        .replace(/&lt;/g, '<')
+                        .replace(/&amp;/g, '&')
+                        .replace(/&quot;/g, '"')
+                        .replace(/&#39;/g, "'");
+                }
+
+                // 先渲染块级公式 $$...$$
+                html = html.replace(/\$\$([\s\S]*?)\$\$/g, function(_, tex) {
+                    try {
+                        const cleaned = decodeEntities(tex.trim());
+                        return window.katex.renderToString(cleaned, { ...katexOptions, displayMode: true });
+                    } catch (e) {
+                        return '<span style="color:red">[KaTeX error]</span>';
+                    }
+                });
+
+                // 再渲染行内公式 $...$
+                html = html.replace(/\$([^\$\n]+?)\$/g, function(_, tex) {
+                    try {
+                        const cleaned = decodeEntities(tex.trim());
+                        return window.katex.renderToString(cleaned, { ...katexOptions, displayMode: false });
+                    } catch (e) {
+                        return '<span style="color:red">[KaTeX error]</span>';
                     }
                 });
-            }, 500);
+            }
+
+            targetEl.innerHTML = html;
+        }
+
+        function renderAll() {
+            const containers = document.querySelectorAll('.kp-markdown-container');
+            containers.forEach((container) => {
+                const sourceEl = container.previousElementSibling;
+                let markdown = '';
+                if (sourceEl && sourceEl.classList.contains('kp-markdown-source')) {
+                    markdown = sourceEl.textContent.trim();
+                }
+                if (!markdown) return;
+                renderMarkdown(markdown, container);
+            });
+        }
+
+        if (document.readyState === 'loading') {
+            document.addEventListener('DOMContentLoaded', function() {
+                waitForLibs(renderAll);
+            });
+        } else {
+            waitForLibs(renderAll);
+        }
+
+        document.addEventListener('livewire:initialized', function() {
+            waitForLibs(renderAll);
         });
+        document.addEventListener('livewire:navigated', () => setTimeout(function() {
+            waitForLibs(renderAll);
+        }, 100));
+    })();
     </script>
 </body>
 </html>

+ 39 - 12
resources/views/pdf/partials/kp-explain-styles.blade.php

@@ -3,22 +3,26 @@
     /* ========== 屏幕预览样式 ========== */
     @media screen {
         body {
-            background: #f5f5f5;
-            padding: 20px;
+            background: #fff;
+            padding: 0;
             font-family: "SimSun", "Songti SC", serif;
+            line-height: 1.65;
+            color: #000;
+            font-size: 14px;
         }
         .page {
             background: #fff;
             max-width: 720px;
             margin: 0 auto;
-            padding: 40px;
-            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
-            border-radius: 4px;
+            padding: 0 12px;
+            box-shadow: none;
+            border-radius: 0;
         }
-        .header {
+        .header,
+        .kp-explain-header {
             text-align: center;
-            margin-bottom: 30px;
-            padding-bottom: 20px;
+            margin-bottom: 1.5rem;
+            padding-bottom: 1rem;
             border-bottom: 2px solid #000;
         }
         .school-name {
@@ -27,10 +31,19 @@
             margin-bottom: 15px;
         }
         .paper-title {
-            font-size: 22px;
+            font-size: 20px;
             font-weight: bold;
             margin-bottom: 15px;
         }
+        .kp-explain-title {
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 10px;
+        }
+        .kp-explain-subtitle {
+            font-size: 14px;
+            color: #666;
+        }
         .info-row {
             display: flex;
             justify-content: space-between;
@@ -44,6 +57,10 @@
         body {
             background: #fff;
             padding: 0;
+            font-family: "SimSun", "Songti SC", serif;
+            line-height: 1.65;
+            color: #000;
+            font-size: 14px;
         }
         .page {
             width: 100%;
@@ -52,10 +69,11 @@
             padding: 0;
             box-shadow: none;
         }
-        .header {
+        .header,
+        .kp-explain-header {
             text-align: center;
-            margin-bottom: 20px;
-            padding-bottom: 15px;
+            margin-bottom: 1.5rem;
+            padding-bottom: 1rem;
             border-bottom: 2px solid #000;
         }
         .school-name {
@@ -68,6 +86,15 @@
             font-weight: bold;
             margin-bottom: 15px;
         }
+        .kp-explain-title {
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 10px;
+        }
+        .kp-explain-subtitle {
+            font-size: 14px;
+            color: #666;
+        }
         .info-row {
             display: flex;
             justify-content: space-between;