paper-body.blade.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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. $boxCounter = app(\App\Support\GradingMarkBoxCounter::class);
  63. $layoutDeciderService = app(\App\Support\OptionLayoutDecider::class);
  64. // 与判题卡共用同一计数规则,避免方框数量不一致
  65. $countBlanks = fn($text) => $boxCounter->countFillBlanks($text);
  66. $renderBoxes = function($num) {
  67. // 判卷方框放大 1.2 倍,保持单行布局
  68. if ($num == 2) {
  69. // 两个方框时,使用右对齐布局
  70. return '<div style="display:flex;justify-content:flex-end;gap:4px;">' .
  71. str_repeat('<span style="display:inline-block;width:17px;height:17px;line-height:17px;border:1px solid #333;"></span>', $num) .
  72. '</div>';
  73. }
  74. 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);
  75. };
  76. @endphp
  77. {{-- 【新增】步骤方框CSS样式 --}}
  78. <style>
  79. .solution-step {
  80. display: block;
  81. margin: 8px 0;
  82. padding: 4px 0;
  83. }
  84. .step-box {
  85. display: inline-block;
  86. margin-right: 8px;
  87. vertical-align: middle;
  88. }
  89. .step-label {
  90. white-space: normal;
  91. vertical-align: middle;
  92. }
  93. .solution-section {
  94. margin: 10px 0;
  95. padding: 8px;
  96. background-color: #f9f9f9;
  97. }
  98. </style>
  99. <!-- 动态大题号 - 选择题 -->
  100. @if($sectionNumbers['choice'] !== null)
  101. <div class="section-title">{{ $getSectionTitle('choice', $sectionNumbers['choice']) }}
  102. @if(count($choiceQuestions) > 0)
  103. @php
  104. $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $choiceQuestions));
  105. @endphp
  106. (本大题共 {{ count($choiceQuestions) }} 小题,共 {{ $choiceTotal }} 分)
  107. @else
  108. (本大题共 0 小题,共 0 分)
  109. @endif
  110. </div>
  111. @if(count($choiceQuestions) > 0)
  112. @foreach($choiceQuestions as $index => $q)
  113. @php
  114. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  115. $questionNumber = $q->question_number ?? ($index + 1);
  116. $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', $q->content);
  117. $cleanContent = trim($cleanContent);
  118. $options = $q->options ?? [];
  119. if (empty($options)) {
  120. // 【修复】选项标记必须在行首或空白后,避免误匹配 SVG 注释中的 BD:DC 等内容
  121. $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
  122. if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
  123. foreach ($matches as $match) {
  124. $optionText = trim($match[2]);
  125. if (!empty($optionText)) {
  126. $options[] = $optionText;
  127. }
  128. }
  129. }
  130. }
  131. $stemLine = $cleanContent;
  132. if (!empty($options)) {
  133. // 【修复】只匹配行首或空白后的选项标记,避免误匹配 SVG 注释中的 BD:DC 等内容
  134. if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
  135. $stemLine = trim($stemMatch[1]);
  136. }
  137. }
  138. // 选择题只做占位符归一,不再兜底追加下划线,避免出现“( )+下划线”重复。
  139. [$renderedStem] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan($stemLine, null, true, false);
  140. // 选择题:句尾不保留句号。
  141. $renderedStem = \App\Support\BlankPlaceholderRenderer::normalizeTerminalPunctuation($renderedStem, 'remove');
  142. $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
  143. @endphp
  144. <div class="question">
  145. <div class="question-grid">
  146. <div class="question-lead">
  147. @if($gradingMode)
  148. <span class="grading-boxes">{!! $renderBoxes(1) !!}</span>
  149. @endif
  150. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  151. </div>
  152. @if(!$gradingMode || $showGradingStem)
  153. <div class="question-main">
  154. <span class="question-stem">{!! $renderedStem !!}</span>
  155. @if($showQuestionId && !empty($q->id))
  156. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  157. @if($showQuestionDifficulty)
  158. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  159. @endif
  160. @endif
  161. </div>
  162. @endif
  163. @if((!$gradingMode || $showGradingStem) && !empty($options))
  164. @php
  165. $layoutMeta = $layoutDeciderService->decide(
  166. $options,
  167. $gradingMode ? 'grading' : 'exam'
  168. );
  169. $optionsClass = $layoutMeta['class'];
  170. $layoutDesc = $layoutMeta['layout'];
  171. \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
  172. 'question_number' => $questionNumber,
  173. 'context' => $gradingMode ? 'grading' : 'exam',
  174. 'opt_count' => $layoutMeta['opt_count'],
  175. 'max_length' => $layoutMeta['max_length'],
  176. 'has_complex_formula' => $layoutMeta['has_complex_formula'],
  177. 'selected_class' => $optionsClass,
  178. 'layout' => $layoutDesc
  179. ]);
  180. @endphp
  181. <div class="question-lead spacer"></div>
  182. <div class="{{ $optionsClass }}">
  183. @foreach($options as $optIndex => $opt)
  184. @php
  185. // 兼容两种格式:数字索引 (0,1,2,3) 或字母键 (A,B,C,D)
  186. if (is_numeric($optIndex)) {
  187. $label = chr(65 + (int)$optIndex);
  188. } else {
  189. $label = strtoupper($optIndex);
  190. }
  191. // 【修复】根据是否已预处理决定处理方式
  192. $normalizedOpt = (string) $opt;
  193. // 选项内优先使用行内分式,避免 \dfrac 导致单个选项视觉突兀
  194. $normalizedOpt = str_replace('\\dfrac', '\\frac', $normalizedOpt);
  195. $normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
  196. $normalizedOpt = $layoutDeciderService->normalizeCompactMathForDisplay($normalizedOpt);
  197. // 清理来源HTML里可能携带的超大字号,避免单题选项异常放大
  198. $normalizedOpt = preg_replace('/font-size\s*:[^;"]+;?/iu', '', $normalizedOpt);
  199. $normalizedOpt = preg_replace('/line-height\s*:[^;"]+;?/iu', '', $normalizedOpt);
  200. $normalizedOpt = preg_replace('/style\s*=\s*([\'"])\s*\1/iu', '', $normalizedOpt);
  201. if ($mathProcessed) {
  202. // 已预处理:数据已包含处理好的 <img> 和公式,直接使用
  203. $renderedOpt = $normalizedOpt;
  204. } else {
  205. // 未预处理:先转义保护,processFormulas() 内部会解码并处理
  206. $encodedOpt = htmlspecialchars($normalizedOpt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  207. $renderedOpt = \App\Services\MathFormulaProcessor::processFormulas($encodedOpt);
  208. }
  209. // 细粒度控制:短选项(如 1/2、-1/3、x、-x)尽量单行展示,长选项允许换行
  210. $rawOptText = html_entity_decode(strip_tags((string) $opt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
  211. $rawOptText = preg_replace('/\s+/u', '', $rawOptText ?? '');
  212. $rawOptLen = mb_strlen((string) $rawOptText, 'UTF-8');
  213. $isShortOption = $rawOptLen <= 8;
  214. @endphp
  215. <div class="option option-compact">
  216. <strong>{{ $label }}.</strong>
  217. <span class="option-value {{ $isShortOption ? 'option-short' : 'option-long' }}">{!! $renderedOpt !!}</span>
  218. </div>
  219. @endforeach
  220. </div>
  221. @endif
  222. @if($gradingMode)
  223. @php
  224. $solutionText = trim($q->solution ?? '');
  225. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  226. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  227. $solutionHtml = $solutionText === ''
  228. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  229. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  230. @endphp
  231. @if($showGradingStem)
  232. <div class="question-lead spacer"></div>
  233. @endif
  234. <div class="answer-meta">
  235. @php
  236. $choiceAnswerRaw = $layoutDeciderService->normalizeCompactMathForDisplay((string) ($q->answer ?? ''));
  237. @endphp
  238. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? $choiceAnswerRaw : \App\Services\MathFormulaProcessor::processFormulas($choiceAnswerRaw) !!}</span></div>
  239. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  240. </div>
  241. @endif
  242. </div>
  243. </div>
  244. @endforeach
  245. @else
  246. <div class="question">
  247. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  248. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  249. </div>
  250. </div>
  251. @endif
  252. @endif
  253. <!-- 动态大题号 - 填空题 -->
  254. @if($sectionNumbers['fill'] !== null)
  255. <div class="section-title">{{ $getSectionTitle('fill', $sectionNumbers['fill']) }}
  256. @if(count($fillQuestions) > 0)
  257. @php
  258. $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $fillQuestions));
  259. @endphp
  260. (本大题共 {{ count($fillQuestions) }} 小题,共 {{ $fillTotal }} 分)
  261. @else
  262. (本大题共 0 小题,共 0 分)
  263. @endif
  264. </div>
  265. @if(count($fillQuestions) > 0)
  266. @foreach($fillQuestions as $index => $q)
  267. @php
  268. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  269. $questionNumber = $q->question_number ?? (count($choiceQuestions) + $index + 1);
  270. $blankSpan = \App\Support\BlankPlaceholderRenderer::defaultBlankSpan();
  271. [$renderedContent, $hasPlaceholders] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan((string) $q->content, $blankSpan, false, false);
  272. // 填空题保留兜底:题干无任何占位时,在末尾补一个标准空位。
  273. if (!$hasPlaceholders) {
  274. $renderedContent .= ' ' . $blankSpan;
  275. }
  276. // 填空题:句尾统一为实心小圆点(英文句点)。
  277. $renderedContent = \App\Support\BlankPlaceholderRenderer::normalizeTerminalPunctuation($renderedContent, 'dot');
  278. // 填空题:若末尾是“句号 + 括注说明”(后可跟图片),将该句号统一为英文小点。
  279. $renderedContent = \App\Support\BlankPlaceholderRenderer::normalizePeriodBeforeTrailingParentheticalNote($renderedContent, '.');
  280. $renderedContent = \App\Support\BlankPlaceholderRenderer::appendTerminalPunctuationIfMissing($renderedContent, '.');
  281. $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
  282. @endphp
  283. <div class="question">
  284. <div class="question-grid">
  285. <div class="question-lead">
  286. @if($gradingMode)
  287. <span class="grading-boxes">{!! $renderBoxes($countBlanks($q->content ?? '')) !!}</span>
  288. @endif
  289. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  290. </div>
  291. @if(!$gradingMode || $showGradingStem)
  292. <div class="question-main">
  293. <span class="question-stem">{!! $renderedContent !!}</span>
  294. @if($showQuestionId && !empty($q->id))
  295. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  296. @if($showQuestionDifficulty)
  297. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  298. @endif
  299. @endif
  300. </div>
  301. @endif
  302. @if($gradingMode)
  303. @php
  304. $solutionText = trim($q->solution ?? '');
  305. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  306. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  307. $solutionHtml = $solutionText === ''
  308. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  309. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  310. @endphp
  311. @if($showGradingStem)
  312. <div class="question-lead spacer"></div>
  313. @endif
  314. <div class="answer-meta">
  315. @php
  316. $fillAnswerRaw = $layoutDeciderService->normalizeCompactMathForDisplay((string) ($q->answer ?? ''));
  317. @endphp
  318. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? $fillAnswerRaw : \App\Services\MathFormulaProcessor::processFormulas($fillAnswerRaw) !!}</span></div>
  319. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  320. </div>
  321. @endif
  322. </div>
  323. </div>
  324. @endforeach
  325. @else
  326. <div class="question">
  327. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  328. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  329. </div>
  330. </div>
  331. @endif
  332. @endif
  333. <!-- 动态大题号 - 解答题 -->
  334. @if($sectionNumbers['answer'] !== null)
  335. <div class="section-title">{{ $getSectionTitle('answer', $sectionNumbers['answer']) }}
  336. @if(count($answerQuestions) > 0)
  337. (本大题共 {{ count($answerQuestions) }} 小题,共 {{ array_sum(array_column($answerQuestions, 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
  338. @else
  339. (本大题共 0 小题,共 0 分)
  340. @endif
  341. </div>
  342. @if(count($answerQuestions) > 0)
  343. @foreach($answerQuestions as $index => $q)
  344. @php
  345. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  346. $questionNumber = $q->question_number ?? (count($choiceQuestions) + count($fillQuestions) + $index + 1);
  347. // 解答题小题排版优化(仅在小题编号语境下换行,避免误伤 f(1) 这类函数表达)
  348. $answerStem = (string) ($q->content ?? '');
  349. preg_match_all('/[((][1-9][0-9]*[))]/u', $answerStem, $subQuestionMatches);
  350. $subQuestionCount = count($subQuestionMatches[0] ?? []);
  351. // 前提:只有出现“至少两个小题编号(形成系列)”才做自动换行
  352. if ($subQuestionCount >= 2) {
  353. // 开头的 (1)/(2) 不额外插入换行,只做标准化空格
  354. $answerStem = preg_replace('/^\s*([((][1-9][0-9]*[))])\s*/u', '$1 ', $answerStem) ?? $answerStem;
  355. // 句读后接小题编号:断行
  356. $answerStem = preg_replace('/([。;;!?!?::.])\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>$2 ', $answerStem) ?? $answerStem;
  357. // 换行簇(实际换行、字面量\\n、已有<br>)后接小题编号:统一压成单个 <br>
  358. $answerStem = preg_replace('/(?:(?:\\\\r\\\\n|\\\\n)|(?:\r?\n)|(?:<br\s*\/?>)|\s)+\s*([((][1-9][0-9]*[))])\s*/u', '<br>$1 ', $answerStem) ?? $answerStem;
  359. // 关键词引导的小题也换行,如 “求(1)…(2)… / 写出(1)…”
  360. $answerStem = preg_replace('/(求出|求解|求|写出|计算|证明|判断|化简)\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>$2 ', $answerStem) ?? $answerStem;
  361. }
  362. $answerStemRendered = $mathProcessed
  363. ? $answerStem
  364. : \App\Services\MathFormulaProcessor::processFormulas($answerStem);
  365. @endphp
  366. <div class="question">
  367. <div class="question-grid">
  368. <div class="question-lead">
  369. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  370. </div>
  371. @if(!$gradingMode || $showGradingStem)
  372. <div class="question-main">
  373. @unless($gradingMode)
  374. <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)
  375. @if($showQuestionId && !empty($q->id))
  376. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  377. @if($showQuestionDifficulty)
  378. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  379. @endif
  380. @endif
  381. </span>
  382. @endunless
  383. <span class="question-stem">{!! $answerStemRendered !!}</span>
  384. </div>
  385. @endif
  386. @unless($gradingMode)
  387. <div class="question-lead spacer"></div>
  388. <div class="answer-area boxy">
  389. <span class="answer-label">作答</span>
  390. </div>
  391. @endunless
  392. @if($gradingMode)
  393. @php
  394. $solutionRaw = $q->solution ?? '';
  395. $solutionProcessed = $mathProcessed ? $solutionRaw : \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
  396. // 去掉分步得分等分值标记
  397. $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
  398. // 【修复】优化解析分段格式 - 支持两种格式:
  399. // 1. 【解题思路】格式
  400. // 2. 解题过程:格式
  401. // 先处理【】格式
  402. $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', "\n\n===SECTION_START===\n【$1】\n===SECTION_END===\n\n", $solutionProcessed);
  403. // 【扩展】处理多种"解题过程"格式,包括带括号的内容
  404. $solutionProcessed = preg_replace('/(解题过程\s*[^:\n]*:)/u', "\n\n===SECTION_START===\n【解题过程】\n===SECTION_END===\n\n", $solutionProcessed);
  405. // 按section分割内容
  406. $sections = explode('===SECTION_START===', $solutionProcessed);
  407. $processedSections = [];
  408. $stepPattern = '(步骤\s*[0-9一二三四五六七八九十百零两]+\s*[::]?|第\s*[0-9一二三四五六七八九十百零两]+\s*步\s*[::]?)';
  409. foreach ($sections as $section) {
  410. if (empty(trim($section))) continue;
  411. // 去掉结尾标记
  412. $section = str_replace('===SECTION_END===', '', $section);
  413. // 检查是否是解题相关部分
  414. if (preg_match('/【(解题思路|详细解答|最终答案|解题过程)】/u', $section, $matches)) {
  415. $sectionTitle = $matches[0];
  416. $sectionContent = preg_replace('/【(解题思路|详细解答|最终答案|解题过程)】/u', '', $section);
  417. // 【修复】处理步骤 - 在每个"步骤N"或"第N步"前添加方框
  418. // 【优化】使用split分割步骤,为所有步骤添加方框(包括第一个)
  419. if (preg_match('/' . $stepPattern . '/u', $sectionContent)) {
  420. // 使用前瞻断言分割,保留分隔符
  421. $allSteps = preg_split('/(?=' . $stepPattern . ')/u', $sectionContent, -1, PREG_SPLIT_NO_EMPTY);
  422. if (count($allSteps) > 0) {
  423. $processed = '';
  424. // 与判题卡计数规则一致:仅显式步骤前加方框;前置引导语不加方框
  425. for ($i = 0; $i < count($allSteps); $i++) {
  426. $stepText = trim($allSteps[$i]);
  427. if (!empty($stepText)) {
  428. $prefix = ($i > 0) ? '<br>' : '';
  429. $isStep = preg_match('/^' . $stepPattern . '/u', $stepText);
  430. if ($isStep) {
  431. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  432. } else {
  433. $processed .= $prefix . '<span class="step-label">' . $stepText . '</span>';
  434. }
  435. }
  436. }
  437. $sectionContent = $processed;
  438. }
  439. } else {
  440. // 没有明确步骤:在标题后添加一个方框作为开始
  441. $sectionContent = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ' . trim($sectionContent);
  442. }
  443. // 包装section
  444. $processedSections[] = '<div class="solution-section"><strong>' . $sectionTitle . '</strong><br>' . $sectionContent . '</div>';
  445. } else {
  446. // 非解题部分直接保留
  447. $processedSections[] = $section;
  448. }
  449. }
  450. // 重新组合所有部分
  451. $solutionProcessed = implode('', $processedSections);
  452. // 【新增】如果没有匹配到任何section标记,整个solution都没有方框,则默认加一个方框
  453. if (empty($processedSections) || (count($processedSections) === 1 && !str_contains($processedSections[0] ?? '', 'step-box'))) {
  454. // 检查是否有步骤关键词
  455. if (preg_match('/' . $stepPattern . '/u', $solutionProcessed)) {
  456. // 有步骤关键词:为每个步骤添加方框
  457. $allSteps = preg_split('/(?=' . $stepPattern . ')/u', $solutionProcessed, -1, PREG_SPLIT_NO_EMPTY);
  458. if (count($allSteps) > 0) {
  459. $processed = '';
  460. for ($i = 0; $i < count($allSteps); $i++) {
  461. $stepText = trim($allSteps[$i]);
  462. if (!empty($stepText)) {
  463. // 只有真正以"步骤"或"第X步"开头的部分才加方框
  464. // 第一个部分如果不是步骤开头(如【分析】),则不加方框
  465. $isStep = preg_match('/^' . $stepPattern . '/u', $stepText);
  466. $prefix = ($i > 0) ? '<br>' : '';
  467. if ($isStep) {
  468. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  469. } else {
  470. // 非步骤的前缀文本,直接输出不加方框
  471. $processed .= $prefix . '<span class="step-label">' . $stepText . '</span>';
  472. }
  473. }
  474. }
  475. $solutionProcessed = $processed;
  476. }
  477. } else {
  478. // 没有步骤关键词:默认在开头添加一个方框
  479. $solutionProcessed = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . trim($solutionProcessed) . '</span></span>';
  480. }
  481. }
  482. // 将多余的换行转换为<br>,但保留合理的段落间距
  483. $solutionProcessed = preg_replace('/\n{3,}/u', "\n\n", $solutionProcessed);
  484. $solutionProcessed = nl2br($solutionProcessed);
  485. @endphp
  486. @if($showGradingStem)
  487. <div class="question-lead spacer"></div>
  488. @endif
  489. <div class="answer-meta">
  490. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  491. <div class="answer-line solution-parsed">{!! $solutionProcessed !!}</div>
  492. </div>
  493. @endif
  494. </div>
  495. </div>
  496. @endforeach
  497. @else
  498. <div class="question">
  499. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  500. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  501. </div>
  502. </div>
  503. @endif
  504. @endif