paper-body.blade.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. @php
  2. $choiceQuestions = $questions['choice'] ?? [];
  3. $fillQuestions = $questions['fill'] ?? [];
  4. $answerQuestions = $questions['answer'] ?? [];
  5. $gradingMode = $grading ?? false;
  6. // 是否在题干后显示题目ID (Q000123)
  7. $showQuestionId = config('exam.show_question_id_in_pdf', false);
  8. // 是否在题号后显示题目难度(用于校验排序)
  9. $showQuestionDifficulty = config('exam.show_question_difficulty_in_pdf', false);
  10. // 判卷模式是否显示题目题干与选项,默认显示;可通过 EXAM_PDF_GRADING_SHOW_STEM 关闭
  11. $showGradingStem = config('exam.pdf_grading_show_stem', true);
  12. // 格式化题目ID为6位补0格式
  13. $formatQuestionId = function($id) {
  14. if (empty($id)) return '';
  15. return '(Q' . str_pad($id, 6, '0', STR_PAD_LEFT) . ')';
  16. };
  17. $formatDifficulty = function($difficulty) {
  18. if ($difficulty === null || $difficulty === '') return '';
  19. $value = (float) $difficulty;
  20. return sprintf('[%.2f]', $value);
  21. };
  22. // 【新增】动态计算大题号 - 根据有题目的题型分配序号
  23. $sectionNumbers = [
  24. 'choice' => null,
  25. 'fill' => null,
  26. 'answer' => null
  27. ];
  28. $currentSectionNumber = 1;
  29. // 只给有题目的题型分配序号
  30. if (!empty($choiceQuestions)) {
  31. $sectionNumbers['choice'] = $currentSectionNumber++;
  32. }
  33. if (!empty($fillQuestions)) {
  34. $sectionNumbers['fill'] = $currentSectionNumber++;
  35. }
  36. if (!empty($answerQuestions)) {
  37. $sectionNumbers['answer'] = $currentSectionNumber++;
  38. }
  39. // 获取题型名称的辅助函数
  40. $getSectionTitle = function($type, $sectionNumber) {
  41. $typeNames = [
  42. 'choice' => '选择题',
  43. 'fill' => '填空题',
  44. 'answer' => '解答题'
  45. ];
  46. // 将数字转换为中文数字
  47. $chineseNumbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
  48. $chineseNumber = $chineseNumbers[$sectionNumber] ?? (string)$sectionNumber;
  49. return $chineseNumber . '、' . $typeNames[$type];
  50. };
  51. // 检查是否有数学公式处理标记,避免重复处理
  52. $mathProcessed = false;
  53. // 检查所有题型中是否有任何题目包含 math_processed 标记
  54. foreach ([$choiceQuestions, $fillQuestions, $answerQuestions] as $questionType) {
  55. foreach ($questionType as $q) {
  56. if (isset($q->math_processed) && $q->math_processed) {
  57. $mathProcessed = true;
  58. break 2; // 找到标记就退出两层循环
  59. }
  60. }
  61. }
  62. // 计算填空空格数量
  63. $countBlanks = function($text) {
  64. $count = 0;
  65. $count += preg_match_all('/_{2,}/u', $text, $m);
  66. $count += preg_match_all('/(\s*)/u', $text, $m);
  67. $count += preg_match_all('/\(\s*\)/', $text, $m);
  68. return max(1, $count);
  69. };
  70. // 计算步骤数量
  71. $countSteps = function($text) {
  72. $matches = [];
  73. $cnt = preg_match_all('/第\s*\d+\s*步/u', $text ?? '', $matches);
  74. return max(1, $cnt);
  75. };
  76. $renderBoxes = function($num) {
  77. // 判卷方框放大 1.2 倍,保持单行布局
  78. if ($num == 2) {
  79. // 两个方框时,使用右对齐布局
  80. return '<div style="display:flex;justify-content:flex-end;gap:4px;">' .
  81. str_repeat('<span style="display:inline-block;width:17px;height:17px;line-height:17px;border:1px solid #333;"></span>', $num) .
  82. '</div>';
  83. }
  84. return str_repeat('<span style="display:inline-block;width:17px;height:17px;line-height:17px;border:1px solid #333;margin-right:4px;vertical-align:middle;"></span>', $num);
  85. };
  86. @endphp
  87. {{-- 【新增】步骤方框CSS样式 --}}
  88. <style>
  89. .solution-step {
  90. display: block;
  91. margin: 8px 0;
  92. padding: 4px 0;
  93. }
  94. .step-box {
  95. display: inline-block;
  96. margin-right: 8px;
  97. vertical-align: middle;
  98. }
  99. .step-label {
  100. white-space: normal;
  101. vertical-align: middle;
  102. }
  103. .solution-section {
  104. margin: 10px 0;
  105. padding: 8px;
  106. background-color: #f9f9f9;
  107. }
  108. </style>
  109. <!-- 动态大题号 - 选择题 -->
  110. @if($sectionNumbers['choice'] !== null)
  111. <div class="section-title">{{ $getSectionTitle('choice', $sectionNumbers['choice']) }}
  112. @if(count($choiceQuestions) > 0)
  113. @php
  114. $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $choiceQuestions));
  115. @endphp
  116. (本大题共 {{ count($choiceQuestions) }} 小题,共 {{ $choiceTotal }} 分)
  117. @else
  118. (本大题共 0 小题,共 0 分)
  119. @endif
  120. </div>
  121. @if(count($choiceQuestions) > 0)
  122. @foreach($choiceQuestions as $index => $q)
  123. @php
  124. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  125. $questionNumber = $q->question_number ?? ($index + 1);
  126. $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', $q->content);
  127. $cleanContent = trim($cleanContent);
  128. $options = $q->options ?? [];
  129. if (empty($options)) {
  130. // 【修复】选项标记必须在行首或空白后,避免误匹配 SVG 注释中的 BD:DC 等内容
  131. $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
  132. if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
  133. foreach ($matches as $match) {
  134. $optionText = trim($match[2]);
  135. if (!empty($optionText)) {
  136. $options[] = $optionText;
  137. }
  138. }
  139. }
  140. }
  141. $stemLine = $cleanContent;
  142. if (!empty($options)) {
  143. // 【修复】只匹配行首或空白后的选项标记,避免误匹配 SVG 注释中的 BD:DC 等内容
  144. if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
  145. $stemLine = trim($stemMatch[1]);
  146. }
  147. }
  148. // 将题干中的空括号/下划线替换为短波浪线;如无占位符,则在末尾追加短波浪线
  149. $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
  150. // 【修复】扩展下划线转换规则,支持LaTeX格式和多种占位符
  151. $renderedStem = $stemLine;
  152. // 先处理LaTeX格式的underline命令
  153. $renderedStem = preg_replace('/\\\underline\{[^}]*\}/', $blankSpan, $renderedStem);
  154. $renderedStem = preg_replace('/\\\qquad+/', $blankSpan, $renderedStem);
  155. // 【修复】在处理填空占位符时,保护LaTeX公式不被破坏
  156. // 先标记LaTeX公式区域
  157. $latexPlaceholders = [];
  158. $counter = 0;
  159. $renderedStem = preg_replace_callback('/\$[^$]+\$/u', function($matches) use (&$latexPlaceholders, &$counter, $blankSpan) {
  160. $placeholder = '<<<LATEX_' . $counter . '>>>';
  161. $latexPlaceholders[$placeholder] = $matches[0];
  162. $counter++;
  163. return $placeholder;
  164. }, $renderedStem);
  165. // 现在处理普通占位符(不会破坏LaTeX公式)
  166. $renderedStem = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedStem);
  167. // 恢复LaTeX公式(并进行HTML实体编码防止被浏览器解析)
  168. foreach ($latexPlaceholders as $placeholder => $latexContent) {
  169. $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  170. $renderedStem = str_replace($placeholder, $encodedLatex, $renderedStem);
  171. }
  172. // 如果没有占位符,在末尾添加
  173. if ($renderedStem === $stemLine) {
  174. $renderedStem .= ' ' . $blankSpan;
  175. }
  176. $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
  177. @endphp
  178. <div class="question">
  179. <div class="question-grid">
  180. <div class="question-lead">
  181. @if($gradingMode)
  182. <span class="grading-boxes">{!! $renderBoxes(1) !!}</span>
  183. @endif
  184. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  185. </div>
  186. @if(!$gradingMode || $showGradingStem)
  187. <div class="question-main">
  188. <span class="question-stem">{!! $renderedStem !!}</span>
  189. @if($showQuestionId && !empty($q->id))
  190. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  191. @if($showQuestionDifficulty)
  192. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  193. @endif
  194. @endif
  195. </div>
  196. @endif
  197. @if((!$gradingMode || $showGradingStem) && !empty($options))
  198. @php
  199. // 计算选项长度并动态选择布局
  200. $optCount = count($options);
  201. $maxOptionLength = 0;
  202. foreach ($options as $opt) {
  203. // 用原始选项文本估算长度,避免公式渲染后的冗余DOM文本干扰列数判断
  204. $optText = strip_tags((string) $opt);
  205. $optText = preg_replace('/\\\\[a-zA-Z]+|[\\{\\}\\$\\^_]/u', '', $optText);
  206. $optText = preg_replace('/\s+/u', '', (string) $optText);
  207. $maxOptionLength = max($maxOptionLength, mb_strlen($optText, 'UTF-8'));
  208. }
  209. // 根据最长选项长度和选项数量动态选择布局
  210. // 短选项(≤15字符)且选项数≤4:4列布局
  211. // 中等选项(16-30字符)或选项数>4:2列布局
  212. // 长选项(>30字符):1列布局
  213. if ($maxOptionLength <= 13) {
  214. $optionsClass = 'options-grid-4';
  215. $layoutDesc = '4列布局';
  216. } elseif ($maxOptionLength <= 26) {
  217. $optionsClass = 'options-grid-2';
  218. $layoutDesc = '2列布局';
  219. } else {
  220. $optionsClass = 'options-grid-1';
  221. $layoutDesc = '1列布局';
  222. }
  223. \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
  224. 'question_number' => $questionNumber,
  225. 'opt_count' => $optCount,
  226. 'max_length' => $maxOptionLength,
  227. 'selected_class' => $optionsClass,
  228. 'layout' => $layoutDesc
  229. ]);
  230. @endphp
  231. <div class="question-lead spacer"></div>
  232. <div class="{{ $optionsClass }}">
  233. @foreach($options as $optIndex => $opt)
  234. @php
  235. // 兼容两种格式:数字索引 (0,1,2,3) 或字母键 (A,B,C,D)
  236. if (is_numeric($optIndex)) {
  237. $label = chr(65 + (int)$optIndex);
  238. } else {
  239. $label = strtoupper($optIndex);
  240. }
  241. // 【修复】根据是否已预处理决定处理方式
  242. $normalizedOpt = (string) $opt;
  243. // 选项内优先使用行内分式,避免 \dfrac 导致单个选项视觉突兀
  244. $normalizedOpt = str_replace('\\dfrac', '\\frac', $normalizedOpt);
  245. $normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
  246. // 清理来源HTML里可能携带的超大字号,避免单题选项异常放大
  247. $normalizedOpt = preg_replace('/font-size\s*:[^;"]+;?/iu', '', $normalizedOpt);
  248. $normalizedOpt = preg_replace('/line-height\s*:[^;"]+;?/iu', '', $normalizedOpt);
  249. $normalizedOpt = preg_replace('/style\s*=\s*([\'"])\s*\1/iu', '', $normalizedOpt);
  250. if ($mathProcessed) {
  251. // 已预处理:数据已包含处理好的 <img> 和公式,直接使用
  252. $renderedOpt = $normalizedOpt;
  253. } else {
  254. // 未预处理:先转义保护,processFormulas() 内部会解码并处理
  255. $encodedOpt = htmlspecialchars($normalizedOpt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  256. $renderedOpt = \App\Services\MathFormulaProcessor::processFormulas($encodedOpt);
  257. }
  258. @endphp
  259. <div class="option option-compact">
  260. <strong>{{ $label }}.</strong>&nbsp;{!! $renderedOpt !!}
  261. </div>
  262. @endforeach
  263. </div>
  264. @endif
  265. @if($gradingMode)
  266. @php
  267. $solutionText = trim($q->solution ?? '');
  268. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  269. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  270. $solutionHtml = $solutionText === ''
  271. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  272. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  273. @endphp
  274. @if($showGradingStem)
  275. <div class="question-lead spacer"></div>
  276. @endif
  277. <div class="answer-meta">
  278. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  279. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  280. </div>
  281. @endif
  282. </div>
  283. </div>
  284. @endforeach
  285. @else
  286. <div class="question">
  287. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  288. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  289. </div>
  290. </div>
  291. @endif
  292. @endif
  293. <!-- 动态大题号 - 填空题 -->
  294. @if($sectionNumbers['fill'] !== null)
  295. <div class="section-title">{{ $getSectionTitle('fill', $sectionNumbers['fill']) }}
  296. @if(count($fillQuestions) > 0)
  297. @php
  298. $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $fillQuestions));
  299. @endphp
  300. (本大题共 {{ count($fillQuestions) }} 小题,共 {{ $fillTotal }} 分)
  301. @else
  302. (本大题共 0 小题,共 0 分)
  303. @endif
  304. </div>
  305. @if(count($fillQuestions) > 0)
  306. @foreach($fillQuestions as $index => $q)
  307. @php
  308. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  309. $questionNumber = $q->question_number ?? (count($choiceQuestions) + $index + 1);
  310. $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
  311. // 【修复】扩展下划线转换规则,支持LaTeX格式和多种占位符
  312. $renderedContent = $q->content;
  313. // 【修复】在处理填空占位符时,保护LaTeX公式不被破坏
  314. // 先标记LaTeX公式区域(支持包含反斜杠和花括号的LaTeX命令)
  315. $latexPlaceholders = [];
  316. $counter = 0;
  317. $renderedContent = preg_replace_callback('/\$(?:[^\$]|\\.)*\$/u', function($matches) use (&$latexPlaceholders, &$counter, $blankSpan) {
  318. $placeholder = '<<<LATEX_FILL_' . $counter . '>>>';
  319. $latexPlaceholders[$placeholder] = $matches[0];
  320. $counter++;
  321. return $placeholder;
  322. }, $renderedContent);
  323. // 现在处理普通占位符(不会破坏LaTeX公式)
  324. $renderedContent = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedContent);
  325. // 恢复LaTeX公式(并进行HTML实体编码防止被浏览器解析)
  326. foreach ($latexPlaceholders as $placeholder => $latexContent) {
  327. $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  328. $renderedContent = str_replace($placeholder, $encodedLatex, $renderedContent);
  329. }
  330. // 如果没有占位符且内容没有变化,在末尾添加
  331. // 但要检查是否已经有填空占位符(如\underline{\qquad})
  332. if ($renderedContent === $q->content && !preg_match('/\\\\underline|\\\\qquad|(\s*)|\(\s*\)/', $renderedContent)) {
  333. $renderedContent .= ' ' . $blankSpan;
  334. }
  335. $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
  336. @endphp
  337. <div class="question">
  338. <div class="question-grid">
  339. <div class="question-lead">
  340. @if($gradingMode)
  341. <span class="grading-boxes">{!! $renderBoxes($countBlanks($q->content ?? '')) !!}</span>
  342. @endif
  343. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  344. </div>
  345. @if(!$gradingMode || $showGradingStem)
  346. <div class="question-main">
  347. <span class="question-stem">{!! $renderedContent !!}</span>
  348. @if($showQuestionId && !empty($q->id))
  349. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  350. @if($showQuestionDifficulty)
  351. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  352. @endif
  353. @endif
  354. </div>
  355. @endif
  356. @if($gradingMode)
  357. @php
  358. $solutionText = trim($q->solution ?? '');
  359. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  360. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  361. $solutionHtml = $solutionText === ''
  362. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  363. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  364. @endphp
  365. @if($showGradingStem)
  366. <div class="question-lead spacer"></div>
  367. @endif
  368. <div class="answer-meta">
  369. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  370. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  371. </div>
  372. @endif
  373. </div>
  374. </div>
  375. @endforeach
  376. @else
  377. <div class="question">
  378. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  379. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  380. </div>
  381. </div>
  382. @endif
  383. @endif
  384. <!-- 动态大题号 - 解答题 -->
  385. @if($sectionNumbers['answer'] !== null)
  386. <div class="section-title">{{ $getSectionTitle('answer', $sectionNumbers['answer']) }}
  387. @if(count($answerQuestions) > 0)
  388. (本大题共 {{ count($answerQuestions) }} 小题,共 {{ array_sum(array_column($answerQuestions, 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
  389. @else
  390. (本大题共 0 小题,共 0 分)
  391. @endif
  392. </div>
  393. @if(count($answerQuestions) > 0)
  394. @foreach($answerQuestions as $index => $q)
  395. @php
  396. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  397. $questionNumber = $q->question_number ?? (count($choiceQuestions) + count($fillQuestions) + $index + 1);
  398. @endphp
  399. <div class="question">
  400. <div class="question-grid">
  401. <div class="question-lead">
  402. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  403. </div>
  404. @if(!$gradingMode || $showGradingStem)
  405. <div class="question-main">
  406. @unless($gradingMode)
  407. <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)
  408. @if($showQuestionId && !empty($q->id))
  409. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  410. @if($showQuestionDifficulty)
  411. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  412. @endif
  413. @endif
  414. </span>
  415. @endunless
  416. <span class="question-stem">{!! $mathProcessed ? $q->content : \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
  417. </div>
  418. @endif
  419. @unless($gradingMode)
  420. <div class="question-lead spacer"></div>
  421. <div class="answer-area boxy">
  422. <span class="answer-label">作答</span>
  423. </div>
  424. @endunless
  425. @if($gradingMode)
  426. @php
  427. $solutionRaw = $q->solution ?? '';
  428. $solutionProcessed = $mathProcessed ? $solutionRaw : \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
  429. // 去掉分步得分等分值标记
  430. $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
  431. // 【修复】优化解析分段格式 - 支持两种格式:
  432. // 1. 【解题思路】格式
  433. // 2. 解题过程:格式
  434. // 先处理【】格式
  435. $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', "\n\n===SECTION_START===\n【$1】\n===SECTION_END===\n\n", $solutionProcessed);
  436. // 【扩展】处理多种"解题过程"格式,包括带括号的内容
  437. $solutionProcessed = preg_replace('/(解题过程\s*[^:\n]*:)/u', "\n\n===SECTION_START===\n【解题过程】\n===SECTION_END===\n\n", $solutionProcessed);
  438. // 按section分割内容
  439. $sections = explode('===SECTION_START===', $solutionProcessed);
  440. $processedSections = [];
  441. foreach ($sections as $section) {
  442. if (empty(trim($section))) continue;
  443. // 去掉结尾标记
  444. $section = str_replace('===SECTION_END===', '', $section);
  445. // 检查是否是解题相关部分
  446. if (preg_match('/【(解题思路|详细解答|最终答案|解题过程)】/u', $section, $matches)) {
  447. $sectionTitle = $matches[0];
  448. $sectionContent = preg_replace('/【(解题思路|详细解答|最终答案|解题过程)】/u', '', $section);
  449. // 【修复】处理步骤 - 在每个"步骤N"或"第N步"前添加方框
  450. // 【优化】使用split分割步骤,为所有步骤添加方框(包括第一个)
  451. if (preg_match('/(步骤\s*\d+|第\s*\d+\s*步)/u', $sectionContent)) {
  452. // 使用前瞻断言分割,保留分隔符
  453. $allSteps = preg_split('/(?=步骤\s*\d+|第\s*\d+\s*步)/u', $sectionContent, -1, PREG_SPLIT_NO_EMPTY);
  454. if (count($allSteps) > 0) {
  455. $processed = '';
  456. // 为每个步骤添加方框(包括第一个)
  457. for ($i = 0; $i < count($allSteps); $i++) {
  458. $stepText = trim($allSteps[$i]);
  459. if (!empty($stepText)) {
  460. // 为每个步骤添加方框和换行(第一个步骤前面不加<br>)
  461. $prefix = ($i > 0) ? '<br>' : '';
  462. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  463. }
  464. }
  465. $sectionContent = $processed;
  466. }
  467. } else {
  468. // 没有明确步骤:在标题后添加一个方框作为开始
  469. $sectionContent = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ' . trim($sectionContent);
  470. }
  471. // 包装section
  472. $processedSections[] = '<div class="solution-section"><strong>' . $sectionTitle . '</strong><br>' . $sectionContent . '</div>';
  473. } else {
  474. // 非解题部分直接保留
  475. $processedSections[] = $section;
  476. }
  477. }
  478. // 重新组合所有部分
  479. $solutionProcessed = implode('', $processedSections);
  480. // 【新增】如果没有匹配到任何section标记,整个solution都没有方框,则默认加一个方框
  481. if (empty($processedSections) || (count($processedSections) === 1 && !str_contains($processedSections[0] ?? '', 'step-box'))) {
  482. // 检查是否有步骤关键词
  483. if (preg_match('/(步骤\s*\d+|第\s*\d+\s*步)/u', $solutionProcessed)) {
  484. // 有步骤关键词:为每个步骤添加方框
  485. $allSteps = preg_split('/(?=步骤\s*\d+|第\s*\d+\s*步)/u', $solutionProcessed, -1, PREG_SPLIT_NO_EMPTY);
  486. if (count($allSteps) > 0) {
  487. $processed = '';
  488. for ($i = 0; $i < count($allSteps); $i++) {
  489. $stepText = trim($allSteps[$i]);
  490. if (!empty($stepText)) {
  491. // 只有真正以"步骤"或"第X步"开头的部分才加方框
  492. // 第一个部分如果不是步骤开头(如【分析】),则不加方框
  493. $isStep = preg_match('/^(步骤\s*\d+|第\s*\d+\s*步)/u', $stepText);
  494. $prefix = ($i > 0) ? '<br>' : '';
  495. if ($isStep) {
  496. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  497. } else {
  498. // 非步骤的前缀文本,直接输出不加方框
  499. $processed .= $prefix . '<span class="step-label">' . $stepText . '</span>';
  500. }
  501. }
  502. }
  503. $solutionProcessed = $processed;
  504. }
  505. } else {
  506. // 没有步骤关键词:默认在开头添加一个方框
  507. $solutionProcessed = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . trim($solutionProcessed) . '</span></span>';
  508. }
  509. }
  510. // 将多余的换行转换为<br>,但保留合理的段落间距
  511. $solutionProcessed = preg_replace('/\n{3,}/u', "\n\n", $solutionProcessed);
  512. $solutionProcessed = nl2br($solutionProcessed);
  513. @endphp
  514. @if($showGradingStem)
  515. <div class="question-lead spacer"></div>
  516. @endif
  517. <div class="answer-meta">
  518. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  519. <div class="answer-line solution-parsed">{!! $solutionProcessed !!}</div>
  520. </div>
  521. @endif
  522. </div>
  523. </div>
  524. @endforeach
  525. @else
  526. <div class="question">
  527. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  528. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  529. </div>
  530. </div>
  531. @endif
  532. @endif