paper-body.blade.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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. /** 待入库质检页:整题点击选中(questions_tem 题为负 id,用 abs 对应 tem 主键) */
  67. $interactiveTemSelect = $interactiveTemSelect ?? false;
  68. $selectedTemIdForSelect = $selectedTemIdForSelect ?? null;
  69. /** 多选模式:点击切换,右侧批量入库 */
  70. $interactiveTemMultiSelect = $interactiveTemMultiSelect ?? false;
  71. $selectedTemIdsForMultiSelect = is_array($selectedTemIdsForMultiSelect ?? null) ? $selectedTemIdsForMultiSelect : [];
  72. $renderBoxes = function($num) {
  73. // 判卷方框放大 1.2 倍,保持单行布局
  74. if ($num == 2) {
  75. // 两个方框时,使用右对齐布局
  76. return '<div style="display:flex;justify-content:flex-end;gap:4px;">' .
  77. str_repeat('<span style="display:inline-block;width:17px;height:17px;line-height:17px;border:1px solid #333;"></span>', $num) .
  78. '</div>';
  79. }
  80. 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);
  81. };
  82. @endphp
  83. {{-- 【新增】步骤方框CSS样式 --}}
  84. <style>
  85. .solution-step {
  86. display: block;
  87. margin: 8px 0;
  88. padding: 4px 0;
  89. }
  90. .step-box {
  91. display: inline-block;
  92. margin-right: 8px;
  93. vertical-align: middle;
  94. }
  95. .step-label {
  96. white-space: normal;
  97. vertical-align: middle;
  98. }
  99. .solution-section {
  100. margin: 10px 0;
  101. padding: 8px;
  102. background-color: #f9f9f9;
  103. }
  104. </style>
  105. <!-- 动态大题号 - 选择题 -->
  106. @if($sectionNumbers['choice'] !== null)
  107. <div class="section-title">{{ $getSectionTitle('choice', $sectionNumbers['choice']) }}
  108. @if(count($choiceQuestions) > 0)
  109. @php
  110. $choiceTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $choiceQuestions));
  111. @endphp
  112. (本大题共 {{ count($choiceQuestions) }} 小题,共 {{ $choiceTotal }} 分)
  113. @else
  114. (本大题共 0 小题,共 0 分)
  115. @endif
  116. </div>
  117. @if(count($choiceQuestions) > 0)
  118. @foreach($choiceQuestions as $index => $q)
  119. @php
  120. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  121. $questionNumber = $q->question_number ?? ($index + 1);
  122. $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', $q->content);
  123. $cleanContent = trim($cleanContent);
  124. $options = $q->options ?? [];
  125. if (empty($options)) {
  126. // 【修复】选项标记必须在行首或空白后,避免误匹配 SVG 注释中的 BD:DC 等内容
  127. $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
  128. if (preg_match_all($pattern, $cleanContent, $matches, PREG_SET_ORDER)) {
  129. foreach ($matches as $match) {
  130. $optionText = trim($match[2]);
  131. if (!empty($optionText)) {
  132. $options[] = $optionText;
  133. }
  134. }
  135. }
  136. }
  137. $stemLine = $cleanContent;
  138. if (!empty($options)) {
  139. // 【修复】只匹配行首或空白后的选项标记,避免误匹配 SVG 注释中的 BD:DC 等内容
  140. if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $cleanContent, $stemMatch)) {
  141. $stemLine = trim($stemMatch[1]);
  142. }
  143. }
  144. // 选择题只做占位符归一,不再兜底追加下划线,避免出现“( )+下划线”重复。
  145. [$renderedStem] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan($stemLine, null, true, false);
  146. // 选择题:句尾不保留句号。
  147. $renderedStem = \App\Support\BlankPlaceholderRenderer::normalizeTerminalPunctuation($renderedStem, 'remove');
  148. $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
  149. $qTemIdSelect = $interactiveTemSelect ? abs((int) ($q->id ?? 0)) : 0;
  150. if ($interactiveTemSelect && $interactiveTemMultiSelect) {
  151. $qTemSelected = $qTemIdSelect > 0 && in_array($qTemIdSelect, $selectedTemIdsForMultiSelect, true);
  152. } else {
  153. $qTemSelected = $interactiveTemSelect && (int) ($selectedTemIdForSelect ?? 0) === $qTemIdSelect;
  154. }
  155. @endphp
  156. @if($interactiveTemSelect)
  157. @if($interactiveTemMultiSelect)
  158. {{-- 多选:本地立即切换高亮;服务端返回后 syncTemMultiSelectionJs 会校正 --}}
  159. <div
  160. x-on:click.prevent="$el.classList.toggle('qtr-is-selected'); $wire.toggleTemQuestion({{ $qTemIdSelect }})"
  161. wire:key="qtr-tem-{{ $qTemIdSelect }}"
  162. data-tem-id="{{ $qTemIdSelect }}"
  163. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  164. style="cursor:pointer;"
  165. tabindex="0"
  166. role="button"
  167. >
  168. @else
  169. <div
  170. x-on:click.prevent="$wire.toggleTemQuestion({{ $qTemIdSelect }})"
  171. wire:key="qtr-tem-{{ $qTemIdSelect }}"
  172. data-tem-id="{{ $qTemIdSelect }}"
  173. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  174. style="cursor:pointer;"
  175. tabindex="0"
  176. role="button"
  177. >
  178. @endif
  179. @else
  180. <div class="question">
  181. @endif
  182. <div class="question-grid">
  183. <div class="question-lead">
  184. @if($gradingMode)
  185. <span class="grading-boxes">{!! $renderBoxes(1) !!}</span>
  186. @endif
  187. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  188. </div>
  189. @if(!$gradingMode || $showGradingStem)
  190. <div class="question-main">
  191. <span class="question-stem">{!! $renderedStem !!}</span>
  192. @if($showQuestionId && !empty($q->id))
  193. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  194. @if($showQuestionDifficulty)
  195. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  196. @endif
  197. @endif
  198. </div>
  199. @endif
  200. @if((!$gradingMode || $showGradingStem) && !empty($options))
  201. @php
  202. $layoutMeta = $layoutDeciderService->decide(
  203. $options,
  204. $gradingMode ? 'grading' : 'exam'
  205. );
  206. $optionsClass = $layoutMeta['class'];
  207. $layoutDesc = $layoutMeta['layout'];
  208. $hasImageOptionInQuestion = false;
  209. foreach ($options as $optRaw) {
  210. if (preg_match('/<(img|image|svg)\\b|data:image\\//i', (string) $optRaw) === 1) {
  211. $hasImageOptionInQuestion = true;
  212. break;
  213. }
  214. }
  215. if ($hasImageOptionInQuestion) {
  216. // 简化规则:图片选项固定四列同一行展示
  217. $optionsClass = 'options-grid-4';
  218. $layoutDesc = '4列布局(图片选项固定)';
  219. }
  220. \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
  221. 'question_number' => $questionNumber,
  222. 'context' => $gradingMode ? 'grading' : 'exam',
  223. 'opt_count' => $layoutMeta['opt_count'],
  224. 'max_length' => $layoutMeta['max_length'],
  225. 'has_complex_formula' => $layoutMeta['has_complex_formula'],
  226. 'has_image_option' => $hasImageOptionInQuestion,
  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. $normalizedOpt = $layoutDeciderService->normalizeCompactMathForDisplay($normalizedOpt);
  247. // 清理来源HTML里可能携带的超大字号,避免单题选项异常放大
  248. $normalizedOpt = preg_replace('/font-size\s*:[^;"]+;?/iu', '', $normalizedOpt);
  249. $normalizedOpt = preg_replace('/line-height\s*:[^;"]+;?/iu', '', $normalizedOpt);
  250. $normalizedOpt = preg_replace('/style\s*=\s*([\'"])\s*\1/iu', '', $normalizedOpt);
  251. if ($mathProcessed) {
  252. // 已预处理:数据已包含处理好的 <img> 和公式,直接使用
  253. $renderedOpt = $normalizedOpt;
  254. } else {
  255. // 未预处理:先转义保护,processFormulas() 内部会解码并处理
  256. $encodedOpt = htmlspecialchars($normalizedOpt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  257. $renderedOpt = \App\Services\MathFormulaProcessor::processFormulas($encodedOpt);
  258. }
  259. // 仅针对“选项图片”覆盖公式处理器默认的题干尺寸,避免四列布局被 220px 宽图撑出边界
  260. $renderedOpt = preg_replace('/max-width\s*:\s*220px\s*;?/iu', 'max-width:100%;', (string) $renderedOpt);
  261. $renderedOpt = preg_replace('/max-height\s*:\s*60mm\s*;?/iu', 'max-height:28mm;', (string) $renderedOpt);
  262. // 标记选项内图片,供 PDF 全局宽图放大逻辑识别并跳过
  263. $renderedOpt = preg_replace('/<img\b(?![^>]*\bdata-option-image=)/iu', '<img data-option-image="1"', (string) $renderedOpt);
  264. // 兼容未来选项直接使用 <svg> 的场景,同样打标走选项专用规则
  265. $renderedOpt = preg_replace('/<svg\b(?![^>]*\bdata-option-image=)/iu', '<svg data-option-image="1"', (string) $renderedOpt);
  266. // 细粒度控制:短选项(如 1/2、-1/3、x、-x)尽量单行展示,长选项允许换行
  267. $rawOptText = html_entity_decode(strip_tags((string) $opt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
  268. $rawOptText = preg_replace('/\s+/u', '', $rawOptText ?? '');
  269. $rawOptLen = mb_strlen((string) $rawOptText, 'UTF-8');
  270. $isShortOption = $rawOptLen <= 8;
  271. @endphp
  272. @php $hasImageOption = preg_match('/<(img|image|svg)\\b|data:image\\//i', (string) $renderedOpt) === 1; @endphp
  273. <div class="option option-compact {{ $hasImageOption ? 'option-with-image' : '' }}">
  274. <strong>{{ $label }}.</strong>
  275. <span class="option-value {{ $isShortOption ? 'option-short' : 'option-long' }}">{!! $renderedOpt !!}</span>
  276. </div>
  277. @endforeach
  278. </div>
  279. @endif
  280. @if($gradingMode)
  281. @php
  282. $solutionText = trim($q->solution ?? '');
  283. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  284. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  285. $solutionHtml = $solutionText === ''
  286. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  287. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  288. @endphp
  289. @if($showGradingStem)
  290. <div class="question-lead spacer"></div>
  291. @endif
  292. <div class="answer-meta">
  293. @php
  294. $choiceAnswerRaw = $layoutDeciderService->normalizeCompactMathForDisplay((string) ($q->answer ?? ''));
  295. @endphp
  296. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? $choiceAnswerRaw : \App\Services\MathFormulaProcessor::processFormulas($choiceAnswerRaw) !!}</span></div>
  297. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  298. </div>
  299. @endif
  300. </div>
  301. </div>
  302. @endforeach
  303. @else
  304. <div class="question">
  305. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  306. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  307. </div>
  308. </div>
  309. @endif
  310. @endif
  311. <!-- 动态大题号 - 填空题 -->
  312. @if($sectionNumbers['fill'] !== null)
  313. <div class="section-title">{{ $getSectionTitle('fill', $sectionNumbers['fill']) }}
  314. @if(count($fillQuestions) > 0)
  315. @php
  316. $fillTotal = array_sum(array_map(fn($q) => $q->score ?? 5, $fillQuestions));
  317. @endphp
  318. (本大题共 {{ count($fillQuestions) }} 小题,共 {{ $fillTotal }} 分)
  319. @else
  320. (本大题共 0 小题,共 0 分)
  321. @endif
  322. </div>
  323. @if(count($fillQuestions) > 0)
  324. @foreach($fillQuestions as $index => $q)
  325. @php
  326. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  327. $questionNumber = $q->question_number ?? (count($choiceQuestions) + $index + 1);
  328. $blankSpan = \App\Support\BlankPlaceholderRenderer::defaultBlankSpan();
  329. [$renderedContent, $hasPlaceholders] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan((string) $q->content, $blankSpan, false, false);
  330. // 填空题保留兜底:题干无任何占位时,在末尾补一个标准空位。
  331. if (!$hasPlaceholders) {
  332. $renderedContent .= ' ' . $blankSpan;
  333. }
  334. // 填空题:句尾统一为实心小圆点(英文句点)。
  335. $renderedContent = \App\Support\BlankPlaceholderRenderer::normalizeTerminalPunctuation($renderedContent, 'dot');
  336. // 填空题:若末尾是“句号 + 括注说明”(后可跟图片),将该句号统一为英文小点。
  337. $renderedContent = \App\Support\BlankPlaceholderRenderer::normalizePeriodBeforeTrailingParentheticalNote($renderedContent, '.');
  338. $renderedContent = \App\Support\BlankPlaceholderRenderer::appendTerminalPunctuationIfMissing($renderedContent, '.');
  339. $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);
  340. $qTemIdSelect = $interactiveTemSelect ? abs((int) ($q->id ?? 0)) : 0;
  341. if ($interactiveTemSelect && $interactiveTemMultiSelect) {
  342. $qTemSelected = $qTemIdSelect > 0 && in_array($qTemIdSelect, $selectedTemIdsForMultiSelect, true);
  343. } else {
  344. $qTemSelected = $interactiveTemSelect && (int) ($selectedTemIdForSelect ?? 0) === $qTemIdSelect;
  345. }
  346. @endphp
  347. @if($interactiveTemSelect)
  348. @if($interactiveTemMultiSelect)
  349. <div
  350. x-on:click.prevent="$el.classList.toggle('qtr-is-selected'); $wire.toggleTemQuestion({{ $qTemIdSelect }})"
  351. wire:key="qtr-tem-fill-{{ $qTemIdSelect }}"
  352. data-tem-id="{{ $qTemIdSelect }}"
  353. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  354. style="cursor:pointer;"
  355. tabindex="0"
  356. role="button"
  357. >
  358. @else
  359. <div
  360. x-on:click.prevent="$wire.toggleTemQuestion({{ $qTemIdSelect }})"
  361. wire:key="qtr-tem-fill-{{ $qTemIdSelect }}"
  362. data-tem-id="{{ $qTemIdSelect }}"
  363. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  364. style="cursor:pointer;"
  365. tabindex="0"
  366. role="button"
  367. >
  368. @endif
  369. @else
  370. <div class="question">
  371. @endif
  372. <div class="question-grid">
  373. <div class="question-lead">
  374. @if($gradingMode)
  375. <span class="grading-boxes">{!! $renderBoxes($countBlanks($q->content ?? '')) !!}</span>
  376. @endif
  377. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  378. </div>
  379. @if(!$gradingMode || $showGradingStem)
  380. <div class="question-main">
  381. <span class="question-stem">{!! $renderedContent !!}</span>
  382. @if($showQuestionId && !empty($q->id))
  383. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  384. @if($showQuestionDifficulty)
  385. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  386. @endif
  387. @endif
  388. </div>
  389. @endif
  390. @if($gradingMode)
  391. @php
  392. $solutionText = trim($q->solution ?? '');
  393. // 去掉前置的"解题思路"标签,避免出现"解题思路:【解题思路】"重复
  394. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText);
  395. $solutionHtml = $solutionText === ''
  396. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  397. : ($mathProcessed ? $solutionText : \App\Services\MathFormulaProcessor::processFormulas($solutionText));
  398. @endphp
  399. @if($showGradingStem)
  400. <div class="question-lead spacer"></div>
  401. @endif
  402. <div class="answer-meta">
  403. @php
  404. $fillAnswerRaw = $layoutDeciderService->normalizeCompactMathForDisplay((string) ($q->answer ?? ''));
  405. @endphp
  406. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? $fillAnswerRaw : \App\Services\MathFormulaProcessor::processFormulas($fillAnswerRaw) !!}</span></div>
  407. <div class="answer-line"><strong>解题思路:</strong><span class="solution-content">{!! $solutionHtml !!}</span></div>
  408. </div>
  409. @endif
  410. </div>
  411. </div>
  412. @endforeach
  413. @else
  414. <div class="question">
  415. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  416. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  417. </div>
  418. </div>
  419. @endif
  420. @endif
  421. <!-- 动态大题号 - 解答题 -->
  422. @if($sectionNumbers['answer'] !== null)
  423. <div class="section-title">{{ $getSectionTitle('answer', $sectionNumbers['answer']) }}
  424. @if(count($answerQuestions) > 0)
  425. (本大题共 {{ count($answerQuestions) }} 小题,共 {{ array_sum(array_column($answerQuestions, 'score')) }} 分。解答应写出文字说明、证明过程或演算步骤)
  426. @else
  427. (本大题共 0 小题,共 0 分)
  428. @endif
  429. </div>
  430. @if(count($answerQuestions) > 0)
  431. @foreach($answerQuestions as $index => $q)
  432. @php
  433. // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
  434. $questionNumber = $q->question_number ?? (count($choiceQuestions) + count($fillQuestions) + $index + 1);
  435. $qTemIdSelect = $interactiveTemSelect ? abs((int) ($q->id ?? 0)) : 0;
  436. if ($interactiveTemSelect && $interactiveTemMultiSelect) {
  437. $qTemSelected = $qTemIdSelect > 0 && in_array($qTemIdSelect, $selectedTemIdsForMultiSelect, true);
  438. } else {
  439. $qTemSelected = $interactiveTemSelect && (int) ($selectedTemIdForSelect ?? 0) === $qTemIdSelect;
  440. }
  441. // 解答题小题排版优化(仅在小题编号语境下换行,避免误伤 f(1) 这类函数表达)
  442. $answerStem = (string) ($q->content ?? '');
  443. preg_match_all('/[((][1-9][0-9]*[))]/u', $answerStem, $subQuestionMatches);
  444. $subQuestionCount = count($subQuestionMatches[0] ?? []);
  445. // 前提:只有出现“至少两个小题编号(形成系列)”才做自动换行
  446. if ($subQuestionCount >= 2) {
  447. // 开头的 (1)/(2) 不额外插入换行,只做标准化空格
  448. $answerStem = preg_replace('/^\s*([((][1-9][0-9]*[))])\s*/u', '$1 ', $answerStem) ?? $answerStem;
  449. // 句读后接小题编号:断行
  450. $answerStem = preg_replace('/([。;;!?!?::.])\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>$2 ', $answerStem) ?? $answerStem;
  451. // 换行簇(实际换行、字面量\\n、已有<br>)后接小题编号:统一压成单个 <br>
  452. $answerStem = preg_replace('/(?:(?:\\\\r\\\\n|\\\\n)|(?:\r?\n)|(?:<br\s*\/?>)|\s)+\s*([((][1-9][0-9]*[))])\s*/u', '<br>$1 ', $answerStem) ?? $answerStem;
  453. // 关键词引导的小题也换行,如 “求(1)…(2)… / 写出(1)…”
  454. $answerStem = preg_replace('/(求出|求解|求|写出|计算|证明|判断|化简)\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>$2 ', $answerStem) ?? $answerStem;
  455. }
  456. $answerStemRendered = $mathProcessed
  457. ? $answerStem
  458. : \App\Services\MathFormulaProcessor::processFormulas($answerStem);
  459. @endphp
  460. @if($interactiveTemSelect)
  461. @if($interactiveTemMultiSelect)
  462. <div
  463. x-on:click.prevent="$el.classList.toggle('qtr-is-selected'); $wire.toggleTemQuestion({{ $qTemIdSelect }})"
  464. wire:key="qtr-tem-ans-{{ $qTemIdSelect }}"
  465. data-tem-id="{{ $qTemIdSelect }}"
  466. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  467. style="cursor:pointer;"
  468. tabindex="0"
  469. role="button"
  470. >
  471. @else
  472. <div
  473. x-on:click.prevent="$wire.toggleTemQuestion({{ $qTemIdSelect }})"
  474. wire:key="qtr-tem-ans-{{ $qTemIdSelect }}"
  475. data-tem-id="{{ $qTemIdSelect }}"
  476. class="question qtr-selectable{{ $qTemSelected ? ' qtr-is-selected' : '' }}"
  477. style="cursor:pointer;"
  478. tabindex="0"
  479. role="button"
  480. >
  481. @endif
  482. @else
  483. <div class="question">
  484. @endif
  485. <div class="question-grid">
  486. <div class="question-lead">
  487. <span class="question-number">{{ $gradingMode ? '题目 ' : '' }}{{ $questionNumber }}.</span>
  488. </div>
  489. @if(!$gradingMode || $showGradingStem)
  490. <div class="question-main">
  491. @unless($gradingMode)
  492. <span class="question-score-inline">(本小题满分 {{ $q->score ?? 10 }} 分)
  493. @if($showQuestionId && !empty($q->id))
  494. <span class="question-id" style="font-size:10px;color:#999;margin-left:4px;">{!! $formatQuestionId($q->id) !!}</span>
  495. @if($showQuestionDifficulty)
  496. <span style="font-size:10px;color:#999;margin-left:4px;white-space:nowrap;">{{ $formatDifficulty($q->difficulty ?? null) }}</span>
  497. @endif
  498. @endif
  499. </span>
  500. @endunless
  501. <span class="question-stem">{!! $answerStemRendered !!}</span>
  502. </div>
  503. @endif
  504. @unless($gradingMode)
  505. <div class="question-lead spacer"></div>
  506. <div class="answer-area boxy">
  507. <span class="answer-label">作答</span>
  508. </div>
  509. @endunless
  510. @if($gradingMode)
  511. @php
  512. $solutionRaw = $q->solution ?? '';
  513. $solutionProcessed = $mathProcessed ? $solutionRaw : \App\Services\MathFormulaProcessor::processFormulas($solutionRaw);
  514. // 去掉分步得分等分值标记
  515. $solutionProcessed = preg_replace('/(\s*\d+\s*分\s*)/u', '', $solutionProcessed);
  516. // 【修复】优化解析分段格式 - 支持两种格式:
  517. // 1. 【解题思路】格式
  518. // 2. 解题过程:格式
  519. // 先处理【】格式
  520. $solutionProcessed = preg_replace('/【(解题思路|详细解答|最终答案)】/u', "\n\n===SECTION_START===\n【$1】\n===SECTION_END===\n\n", $solutionProcessed);
  521. // 【扩展】处理多种"解题过程"格式,包括带括号的内容
  522. $solutionProcessed = preg_replace('/(解题过程\s*[^:\n]*:)/u', "\n\n===SECTION_START===\n【解题过程】\n===SECTION_END===\n\n", $solutionProcessed);
  523. // 按section分割内容
  524. $sections = explode('===SECTION_START===', $solutionProcessed);
  525. $processedSections = [];
  526. $stepPattern = '(步骤\s*[0-9一二三四五六七八九十百零两]+\s*[::]?|第\s*[0-9一二三四五六七八九十百零两]+\s*步\s*[::]?)';
  527. foreach ($sections as $section) {
  528. if (empty(trim($section))) continue;
  529. // 去掉结尾标记
  530. $section = str_replace('===SECTION_END===', '', $section);
  531. // 检查是否是解题相关部分
  532. if (preg_match('/【(解题思路|详细解答|最终答案|解题过程)】/u', $section, $matches)) {
  533. $sectionTitle = $matches[0];
  534. $sectionContent = preg_replace('/【(解题思路|详细解答|最终答案|解题过程)】/u', '', $section);
  535. // 【修复】处理步骤 - 在每个"步骤N"或"第N步"前添加方框
  536. // 【优化】使用split分割步骤,为所有步骤添加方框(包括第一个)
  537. if (preg_match('/' . $stepPattern . '/u', $sectionContent)) {
  538. // 使用前瞻断言分割,保留分隔符
  539. $allSteps = preg_split('/(?=' . $stepPattern . ')/u', $sectionContent, -1, PREG_SPLIT_NO_EMPTY);
  540. if (count($allSteps) > 0) {
  541. $processed = '';
  542. // 与判题卡计数规则一致:仅显式步骤前加方框;前置引导语不加方框
  543. for ($i = 0; $i < count($allSteps); $i++) {
  544. $stepText = trim($allSteps[$i]);
  545. if (!empty($stepText)) {
  546. $prefix = ($i > 0) ? '<br>' : '';
  547. $isStep = preg_match('/^' . $stepPattern . '/u', $stepText);
  548. if ($isStep) {
  549. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  550. } else {
  551. $processed .= $prefix . '<span class="step-label">' . $stepText . '</span>';
  552. }
  553. }
  554. }
  555. $sectionContent = $processed;
  556. }
  557. } else {
  558. // 没有明确步骤:在标题后添加一个方框作为开始
  559. $sectionContent = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">&nbsp;</span></span> ' . trim($sectionContent);
  560. }
  561. // 包装section
  562. $processedSections[] = '<div class="solution-section"><strong>' . $sectionTitle . '</strong><br>' . $sectionContent . '</div>';
  563. } else {
  564. // 非解题部分直接保留
  565. $processedSections[] = $section;
  566. }
  567. }
  568. // 重新组合所有部分
  569. $solutionProcessed = implode('', $processedSections);
  570. // 【新增】如果没有匹配到任何section标记,整个solution都没有方框,则默认加一个方框
  571. if (empty($processedSections) || (count($processedSections) === 1 && !str_contains($processedSections[0] ?? '', 'step-box'))) {
  572. // 检查是否有步骤关键词
  573. if (preg_match('/' . $stepPattern . '/u', $solutionProcessed)) {
  574. // 有步骤关键词:为每个步骤添加方框
  575. $allSteps = preg_split('/(?=' . $stepPattern . ')/u', $solutionProcessed, -1, PREG_SPLIT_NO_EMPTY);
  576. if (count($allSteps) > 0) {
  577. $processed = '';
  578. for ($i = 0; $i < count($allSteps); $i++) {
  579. $stepText = trim($allSteps[$i]);
  580. if (!empty($stepText)) {
  581. // 只有真正以"步骤"或"第X步"开头的部分才加方框
  582. // 第一个部分如果不是步骤开头(如【分析】),则不加方框
  583. $isStep = preg_match('/^' . $stepPattern . '/u', $stepText);
  584. $prefix = ($i > 0) ? '<br>' : '';
  585. if ($isStep) {
  586. $processed .= $prefix . '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . $stepText . '</span></span>';
  587. } else {
  588. // 非步骤的前缀文本,直接输出不加方框
  589. $processed .= $prefix . '<span class="step-label">' . $stepText . '</span>';
  590. }
  591. }
  592. }
  593. $solutionProcessed = $processed;
  594. }
  595. } else {
  596. // 没有步骤关键词:默认在开头添加一个方框
  597. $solutionProcessed = '<span class="solution-step"><span class="step-box">' . $renderBoxes(1) . '</span><span class="step-label">' . trim($solutionProcessed) . '</span></span>';
  598. }
  599. }
  600. // 将多余的换行转换为<br>,但保留合理的段落间距
  601. $solutionProcessed = preg_replace('/\n{3,}/u', "\n\n", $solutionProcessed);
  602. $solutionProcessed = nl2br($solutionProcessed);
  603. @endphp
  604. @if($showGradingStem)
  605. <div class="question-lead spacer"></div>
  606. @endif
  607. <div class="answer-meta">
  608. <div class="answer-line"><strong>正确答案:</strong><span class="solution-content">{!! $mathProcessed ? ($q->answer ?? '') : \App\Services\MathFormulaProcessor::processFormulas($q->answer ?? '') !!}</span></div>
  609. <div class="answer-line solution-parsed">{!! $solutionProcessed !!}</div>
  610. </div>
  611. @endif
  612. </div>
  613. </div>
  614. @endforeach
  615. @else
  616. <div class="question">
  617. <div class="question-content" style="font-style: italic; color: #999; padding: 20px; border: 1px dashed #ccc; background: #f9f9f9;">
  618. 该题型正在生成中或暂无题目,请稍后刷新页面查看
  619. </div>
  620. </div>
  621. @endif
  622. @endif