paper-body.blade.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. @php
  2. $choiceQuestions = $questions['choice'] ?? [];
  3. $fillQuestions = $questions['fill'] ?? [];
  4. $answerQuestions = $questions['answer'] ?? [];
  5. $gradingMode = $grading ?? false;
  6. // 检查是否有数学公式处理标记,避免重复处理
  7. $mathProcessed = false;
  8. // 检查所有题型中是否有任何题目包含 math_processed 标记
  9. foreach ([$choiceQuestions, $fillQuestions, $answerQuestions] as $questionType) {
  10. foreach ($questionType as $q) {
  11. if (isset($q->math_processed) && $q->math_processed) {
  12. $mathProcessed = true;
  13. break 2; // 找到标记就退出两层循环
  14. }
  15. }
  16. }
  17. // 计算填空空格数量
  18. $countBlanks = function($text) {
  19. $count = 0;
  20. $count += preg_match_all('/_{2,}/u', $text, $m);
  21. $count += preg_match_all('/(\s*)/u', $text, $m);
  22. $count += preg_match_all('/\(\s*\)/', $text, $m);
  23. return max(1, $count);
  24. };
  25. // 计算步骤数量
  26. $countSteps = function($text) {
  27. $matches = [];
  28. $cnt = preg_match_all('/第\s*\d+\s*步/u', $text ?? '', $matches);
  29. return max(1, $cnt);
  30. };
  31. $renderBoxes = function($num) {
  32. // 判卷方框放大 1.2 倍,保持单行布局
  33. if ($num == 2) {
  34. // 两个方框时,使用右对齐布局
  35. return '<div style="display:flex;justify-content:flex-end;gap:4px;">' .
  36. str_repeat('<span style="display:inline-block;width:17px;height:17px;line-height:17px;border:1px solid #333;"></span>', $num) .
  37. '</div>';
  38. }
  39. 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);
  40. };
  41. @endphp
  42. {{-- 【新增】步骤方框CSS样式 --}}
  43. <style>
  44. .solution-step {
  45. display: block;
  46. margin: 8px 0;
  47. padding: 4px 0;
  48. }
  49. .step-box {
  50. display: inline-block;
  51. margin-right: 8px;
  52. vertical-align: middle;
  53. }
  54. .step-label {
  55. white-space: normal;
  56. vertical-align: middle;
  57. }
  58. .solution-section {
  59. margin: 10px 0;
  60. padding: 8px;
  61. background-color: #f9f9f9;
  62. }
  63. </style>
  64. <!-- 一、选择题 -->
  65. <div class="section-title">一、选择题
  66. @if(count($choiceQuestions) > 0)
  67. @php
  68. $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $choiceQuestions));
  69. @endphp
  70. (本大题共 {{ count($choiceQuestions) }} 小题,共 {{ $choiceTotal }} 分)
  71. @else
  72. (本大题共 0 小题,共 0 分)
  73. @endif
  74. </div>
  75. @if(count($choiceQuestions) > 0)
  76. @foreach($choiceQuestions as $index => $q)
  77. @php
  78. $questionNumber = $index + 1;
  79. $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', $q->content);
  80. $cleanContent = trim($cleanContent);
  81. $options = $q->options ?? [];
  82. if (empty($options)) {
  83. $pattern = '/([A-D])[\.、:.:]\s*(.+?)(?=\s*[A-D][\.、:.:]|$)/su';
  84. if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
  85. foreach ($matches as $match) {
  86. $optionText = trim($match[2]);
  87. if (!empty($optionText)) {
  88. $options[] = $optionText;
  89. }
  90. }
  91. }
  92. }
  93. $stemLine = $cleanContent;
  94. if (!empty($options)) {
  95. if (preg_match('/^(.+?)(?=[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
  96. $stemLine = trim($stemMatch[1]);
  97. }
  98. }
  99. // 将题干中的空括号/下划线替换为短波浪线;如无占位符,则在末尾追加短波浪线
  100. $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
  101. $renderedStem = preg_replace(['/((\s*))/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $stemLine);
  102. if ($renderedStem === $stemLine) {
  103. $renderedStem .= ' ' . $blankSpan;
  104. }
  105. $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
  106. @endphp
  107. <div class="question">
  108. <div class="question-grid">
  109. <div class="question-lead">
  110. @if($gradingMode)
  111. <span class="grading-boxes">{!! $renderBoxes(1) !!}</span>
  112. @endif
  113. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  114. </div>
  115. <div class="question-main">
  116. <span class="question-stem">{!! $renderedStem !!}</span>
  117. </div>
  118. @if(!empty($options))
  119. @php
  120. // 计算选项长度并动态选择布局
  121. $optCount = count($options);
  122. $maxOptionLength = 0;
  123. foreach ($options as $opt) {
  124. $optText = strip_tags(\App\Services\MathFormulaProcessor::processFormulas($opt));
  125. $maxOptionLength = max($maxOptionLength, mb_strlen($optText, 'UTF-8'));
  126. }
  127. // 根据最长选项长度和选项数量动态选择布局
  128. // 短选项(≤15字符)且选项数≤4:4列布局
  129. // 中等选项(16-30字符)或选项数>4:2列布局
  130. // 长选项(>30字符):1列布局
  131. if ($maxOptionLength <= 15 && $optCount <= 4) {
  132. $optionsClass = 'options-grid-4';
  133. $layoutDesc = '4列布局';
  134. } elseif ($maxOptionLength <= 30) {
  135. $optionsClass = 'options-grid-2';
  136. $layoutDesc = '2列布局';
  137. } else {
  138. $optionsClass = 'options-grid-1';
  139. $layoutDesc = '1列布局';
  140. }
  141. \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
  142. 'question_number' => $questionNumber,
  143. 'opt_count' => $optCount,
  144. 'max_length' => $maxOptionLength,
  145. 'selected_class' => $optionsClass,
  146. 'layout' => $layoutDesc
  147. ]);
  148. @endphp
  149. <div class="question-lead spacer"></div>
  150. <div class="{{ $optionsClass }}">
  151. @foreach($options as $optIndex => $opt)
  152. @php $label = chr(65 + (int)$optIndex); @endphp
  153. <div class="option option-compact">
  154. <strong>{{ $label }}.</strong>&nbsp;{!! $mathProcessed ? $opt : \App\Services\MathFormulaProcessor::processFormulas($opt) !!}
  155. </div>
  156. @endforeach
  157. </div>
  158. @endif
  159. @if($gradingMode)
  160. @php
  161. $solutionText = trim($q->solution ?? '');
  162. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  163. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  164. $solutionHtml = $solutionText === ''
  165. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  166. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  167. @endphp
  168. <div class="question-lead spacer"></div>
  169. <div class="answer-meta">
  170. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  171. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  172. </div>
  173. @endif
  174. </div>
  175. </div>
  176. @endforeach
  177. @else
  178. <div class="question">
  179. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  180. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  181. </div>
  182. </div>
  183. @endif
  184. <!-- 二、填空题 -->
  185. <div class="section-title">二、填空题
  186. @if(count($fillQuestions) > 0)
  187. @php
  188. $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $fillQuestions));
  189. @endphp
  190. (本大题共 {{ count($fillQuestions) }} 小题,共 {{ $fillTotal }} 分)
  191. @else
  192. (本大题共 0 小题,共 0 分)
  193. @endif
  194. </div>
  195. @if(count($fillQuestions) > 0)
  196. @foreach($fillQuestions as $index => $q)
  197. @php
  198. $questionNumber = count($choiceQuestions) + $index + 1;
  199. $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
  200. $renderedContent = preg_replace(['/((\s*))/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $q->content);
  201. if ($renderedContent === $q->content) {
  202. $renderedContent .= ' ' . $blankSpan;
  203. }
  204. $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
  205. @endphp
  206. <div class="question">
  207. <div class="question-grid">
  208. <div class="question-lead">
  209. @if($gradingMode)
  210. <span class="grading-boxes">{!! $renderBoxes($countBlanks($q->content ?? '')) !!}</span>
  211. @endif
  212. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  213. </div>
  214. <div class="question-main">
  215. <span class="question-stem">{!! $renderedContent !!}</span>
  216. </div>
  217. @if($gradingMode)
  218. @php
  219. $solutionText = trim($q->solution ?? '');
  220. // 去掉前置的“解题思路”标签,避免出现“解题思路:【解题思路】”重复
  221. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  222. $solutionHtml = $solutionText === ''
  223. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  224. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  225. @endphp
  226. <div class="question-lead spacer"></div>
  227. <div class="answer-meta">
  228. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  229. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  230. </div>
  231. @endif
  232. </div>
  233. </div>
  234. @endforeach
  235. @else
  236. <div class="question">
  237. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  238. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  239. </div>
  240. </div>
  241. @endif
  242. <!-- 三、解答题 -->
  243. <div class="section-title">三、解答题
  244. @if(count($answerQuestions) > 0)
  245. (本大题共 {{ count($answerQuestions) }} 小题,共 {{ array_sum(array_column($answerQuestions, 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
  246. @else
  247. (本大题共 0 小题,共 0 分)
  248. @endif
  249. </div>
  250. @if(count($answerQuestions) > 0)
  251. @foreach($answerQuestions as $index => $q)
  252. @php
  253. $questionNumber = count($choiceQuestions) + count($fillQuestions) + $index + 1;
  254. @endphp
  255. <div class="question">
  256. <div class="question-grid">
  257. <div class="question-lead">
  258. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  259. </div>
  260. <div class="question-main">
  261. @unless($gradingMode)
  262. <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)</span>
  263. @endunless
  264. <span class="question-stem">{!! $mathProcessed ? $q->content : \App\Services\MathFormulaProcessor::processFormulas($q->content) !!}</span>
  265. </div>
  266. @unless($gradingMode)
  267. <div class="question-lead spacer"></div>
  268. <div class="answer-area boxy">
  269. <span class="answer-label">作答</span>
  270. </div>
  271. @endunless
  272. @if($gradingMode)
  273. @php
  274. $solutionRaw = $q->solution ?? '';
  275. $solutionProcessed = $mathProcessed ? $solutionRaw : \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
  276. // 去掉分步得分等分值标记
  277. $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
  278. // 【修复】优化解析分段格式 - 支持两种格式:
  279. // 1. 【解题思路】格式
  280. // 2. 解题过程:格式
  281. // 先处理【】格式
  282. $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', "\n\n===SECTION_START===\n【$1】\n===SECTION_END===\n\n", $solutionProcessed);
  283. // 再处理"解题过程:"格式
  284. $solutionProcessed = preg_replace('/(解题过程\s*:)/u', "\n\n===SECTION_START===\n【解题过程】\n===SECTION_END===\n\n", $solutionProcessed);
  285. // 按section分割内容
  286. $sections = explode('===SECTION_START===', $solutionProcessed);
  287. $processedSections = [];
  288. foreach ($sections as $section) {
  289. if (empty(trim($section))) continue;
  290. // 去掉结尾标记
  291. $section = str_replace('===SECTION_END===', '', $section);
  292. // 检查是否是解题相关部分
  293. if (preg_match('/【(解题思路|详细解答|最终答案|解题过程)】/u', $section, $matches)) {
  294. $sectionTitle = $matches[0];
  295. $sectionContent = preg_replace('/【(解题思路|详细解答|最终答案|解题过程)】/u', '', $section);
  296. // 【修复】处理步骤 - 在每个"步骤N"或"第N步"前添加方框
  297. // 【简化】使用split分割步骤,然后在每个步骤前添加方框
  298. if (preg_match('/(步骤\s*\d+|第\s*\d+\s*步)/u', $sectionContent)) {
  299. // 使用前瞻断言分割,保留分隔符
  300. $allSteps = preg_split('/(?=步骤\s*\d+|第\s*\d+\s*步)/u', $sectionContent, -1, PREG_SPLIT_NO_EMPTY);
  301. if (count($allSteps) > 1) {
  302. // 第一部分通常不是步骤,直接保留
  303. $processed = trim($allSteps[0]);
  304. // 从第二个元素开始,每个都是步骤
  305. for ($i = 1; $i < count($allSteps); $i++) {
  306. $stepText = trim($allSteps[$i]);
  307. if (!empty($stepText)) {
  308. // 在步骤前面添加方框和换行
  309. $processed .= '<br><span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  310. }
  311. }
  312. $sectionContent = $processed;
  313. }
  314. } else {
  315. // 没有明确步骤:在标题后添加一个方框作为开始
  316. $sectionContent = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ' . trim($sectionContent);
  317. }
  318. // 包装section
  319. $processedSections[] = '<div class="solution-section"><strong>' . $sectionTitle . '</strong><br>' . $sectionContent . '</div>';
  320. } else {
  321. // 非解题部分直接保留
  322. $processedSections[] = $section;
  323. }
  324. }
  325. // 重新组合所有部分
  326. $solutionProcessed = implode('', $processedSections);
  327. // 将多余的换行转换为<br>,但保留合理的段落间距
  328. $solutionProcessed = preg_replace('/\n{3,}/u', "\n\n", $solutionProcessed);
  329. $solutionProcessed = nl2br($solutionProcessed);
  330. @endphp
  331. <div class="question-lead spacer"></div>
  332. <div class="answer-meta">
  333. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  334. <div class="answer-line solution-parsed">{!! $solutionProcessed !!}</div>
  335. </div>
  336. @endif
  337. </div>
  338. </div>
  339. @endforeach
  340. @else
  341. <div class="question">
  342. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  343. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  344. </div>
  345. </div>
  346. @endif