paper-body.blade.php 21 KB

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