exam-choice-options.blade.php 5.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. {{--
  2. 与 components/exam/paper-body 选择题选项渲染完全一致(布局 + 公式预处理)。
  3. 参数:
  4. - $options: array 选项(数字下标或 A/B/C/D 键)
  5. - $gradingMode: bool 是否判卷上下文(影响 OptionLayoutDecider)
  6. - $mathProcessed: bool 题目是否已整体预处理公式
  7. - $logQuestionNumber: string|int 仅用于日志标识
  8. - $showLeadSpacer: bool 是否在选项前插入与 question-grid 对齐的隐形占位(嵌入案例行等非 grid 场景可设为 false)
  9. --}}
  10. @php
  11. /** @var array $options */
  12. /** @var bool $gradingMode */
  13. /** @var bool $mathProcessed */
  14. /** @var string|int|null $logQuestionNumber */
  15. /** @var bool $showLeadSpacer */
  16. $showLeadSpacer = $showLeadSpacer ?? true;
  17. $layoutDeciderService = app(\App\Support\OptionLayoutDecider::class);
  18. $layoutMeta = $layoutDeciderService->decide(
  19. $options,
  20. $gradingMode ? 'grading' : 'exam'
  21. );
  22. $optionsClass = $layoutMeta['class'];
  23. $layoutDesc = $layoutMeta['layout'];
  24. $hasImageOptionInQuestion = false;
  25. foreach ($options as $optRaw) {
  26. if (preg_match('/<(img|image|svg)\\b|data:image\\//i', (string) $optRaw) === 1) {
  27. $hasImageOptionInQuestion = true;
  28. break;
  29. }
  30. }
  31. if ($hasImageOptionInQuestion) {
  32. $optionsClass = 'options-grid-4';
  33. $layoutDesc = '4列布局(图片选项固定)';
  34. }
  35. \Illuminate\Support\Facades\Log::debug('选择题布局决策', [
  36. 'question_number' => $logQuestionNumber ?? null,
  37. 'context' => $gradingMode ? 'grading' : 'exam',
  38. 'opt_count' => $layoutMeta['opt_count'],
  39. 'max_length' => $layoutMeta['max_length'],
  40. 'has_complex_formula' => $layoutMeta['has_complex_formula'],
  41. 'has_image_option' => $hasImageOptionInQuestion,
  42. 'selected_class' => $optionsClass,
  43. 'layout' => $layoutDesc,
  44. ]);
  45. @endphp
  46. @if($showLeadSpacer)
  47. <div class="question-lead spacer"></div>
  48. @endif
  49. <div class="{{ $optionsClass }}">
  50. @foreach($options as $optIndex => $opt)
  51. @php
  52. // 兼容两种格式:数字索引 (0,1,2,3) 或字母键 (A,B,C,D)
  53. if (is_numeric($optIndex)) {
  54. $label = chr(65 + (int) $optIndex);
  55. } else {
  56. $label = strtoupper($optIndex);
  57. }
  58. // 【修复】根据是否已预处理决定处理方式
  59. $normalizedOpt = (string) $opt;
  60. // 选项内优先使用行内分式,避免 \dfrac 导致单个选项视觉突兀
  61. $normalizedOpt = str_replace('\\dfrac', '\\frac', $normalizedOpt);
  62. $normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
  63. $normalizedOpt = $layoutDeciderService->normalizeCompactMathForDisplay($normalizedOpt);
  64. // 清理来源HTML里可能携带的超大字号,避免单题选项异常放大
  65. $normalizedOpt = preg_replace('/font-size\s*:[^;"]+;?/iu', '', $normalizedOpt);
  66. $normalizedOpt = preg_replace('/line-height\s*:[^;"]+;?/iu', '', $normalizedOpt);
  67. $normalizedOpt = preg_replace('/style\s*=\s*([\'"])\s*\1/iu', '', $normalizedOpt);
  68. if ($mathProcessed) {
  69. // 已预处理:数据已包含处理好的 <img> 和公式,直接使用
  70. $renderedOpt = $normalizedOpt;
  71. } else {
  72. // 未预处理:先转义保护,processFormulas() 内部会解码并处理
  73. $encodedOpt = htmlspecialchars($normalizedOpt, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  74. $renderedOpt = \App\Services\MathFormulaProcessor::processFormulas($encodedOpt);
  75. }
  76. // 仅针对“选项图片”覆盖公式处理器默认的题干尺寸,避免四列布局被 220px 宽图撑出边界
  77. $renderedOpt = preg_replace('/max-width\s*:\s*220px\s*;?/iu', 'max-width:100%;', (string) $renderedOpt);
  78. $renderedOpt = preg_replace('/max-height\s*:\s*60mm\s*;?/iu', 'max-height:28mm;', (string) $renderedOpt);
  79. // 标记选项内图片,供 PDF 全局宽图放大逻辑识别并跳过
  80. $renderedOpt = preg_replace('/<img\b(?![^>]*\bdata-option-image=)/iu', '<img data-option-image="1"', (string) $renderedOpt);
  81. // 兼容未来选项直接使用 <svg> 的场景,同样打标走选项专用规则
  82. $renderedOpt = preg_replace('/<svg\b(?![^>]*\bdata-option-image=)/iu', '<svg data-option-image="1"', (string) $renderedOpt);
  83. // 细粒度控制:短选项(如 1/2、-1/3、x、-x)尽量单行展示,长选项允许换行
  84. $rawOptText = html_entity_decode(strip_tags((string) $opt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
  85. $rawOptText = preg_replace('/\s+/u', '', $rawOptText ?? '');
  86. $rawOptLen = mb_strlen((string) $rawOptText, 'UTF-8');
  87. $isShortOption = $rawOptLen <= 8;
  88. @endphp
  89. @php $hasImageOption = preg_match('/<(img|image|svg)\\b|data:image\\//i', (string) $renderedOpt) === 1; @endphp
  90. <div class="option option-compact {{ $hasImageOption ? 'option-with-image' : '' }}">
  91. <strong>{{ $label }}.</strong>
  92. <span class="option-value {{ $isShortOption ? 'option-short' : 'option-long' }}">{!! $renderedOpt !!}</span>
  93. </div>
  94. @endforeach
  95. </div>