|
|
@@ -194,14 +194,60 @@
|
|
|
font-size: 12px;
|
|
|
line-height: 1.7;
|
|
|
}
|
|
|
- .question-card .math-content img,
|
|
|
- .question-card img {
|
|
|
- max-width: 100% !important;
|
|
|
- width: auto !important;
|
|
|
- height: auto !important;
|
|
|
+ /* 题干区:与判卷 PDF(exam-grading)一致 — <img>、内嵌 SVG、公式 */
|
|
|
+ .question-card .question-stem svg,
|
|
|
+ .question-card .math-content svg {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ display: block;
|
|
|
+ shape-rendering: geometricPrecision;
|
|
|
+ text-rendering: geometricPrecision;
|
|
|
+ }
|
|
|
+ .question-card .question-stem svg text {
|
|
|
+ font-family: "Noto Serif", "Noto Serif CJK SC", "Noto Sans CJK SC", "Noto Sans", "STSongti-SC", "PingFang SC", "Songti SC", serif !important;
|
|
|
+ font-size: 13px !important;
|
|
|
+ font-weight: bold;
|
|
|
+ dominant-baseline: middle;
|
|
|
+ text-anchor: middle;
|
|
|
+ }
|
|
|
+ .question-card .question-stem svg circle,
|
|
|
+ .question-card .question-stem svg line,
|
|
|
+ .question-card .question-stem svg polygon,
|
|
|
+ .question-card .question-stem svg polyline {
|
|
|
+ shape-rendering: geometricPrecision;
|
|
|
+ }
|
|
|
+ .question-card .question-stem img,
|
|
|
+ .question-card .question-main img {
|
|
|
+ display: block;
|
|
|
+ max-width: 220px;
|
|
|
+ max-height: 60mm;
|
|
|
+ width: auto;
|
|
|
+ height: auto;
|
|
|
+ margin: 6px auto;
|
|
|
+ box-sizing: border-box;
|
|
|
object-fit: contain;
|
|
|
+ -webkit-print-color-adjust: exact;
|
|
|
+ print-color-adjust: exact;
|
|
|
+ image-rendering: -webkit-optimize-contrast;
|
|
|
+ }
|
|
|
+ .question-card .question-stem .katex {
|
|
|
+ font-size: 1em !important;
|
|
|
+ vertical-align: 0;
|
|
|
+ }
|
|
|
+ .question-card .question-stem .katex-display {
|
|
|
+ margin: 0.35em 0 !important;
|
|
|
+ }
|
|
|
+ .question-card .solution-content img,
|
|
|
+ .question-card .report-answer-meta img {
|
|
|
display: block;
|
|
|
+ max-width: 220px;
|
|
|
+ max-height: 60mm;
|
|
|
+ width: auto;
|
|
|
+ height: auto;
|
|
|
margin: 6px auto;
|
|
|
+ object-fit: contain;
|
|
|
+ -webkit-print-color-adjust: exact;
|
|
|
+ print-color-adjust: exact;
|
|
|
}
|
|
|
.solution-content {
|
|
|
display: block;
|
|
|
@@ -212,6 +258,58 @@
|
|
|
page-break-inside: auto;
|
|
|
break-inside: auto;
|
|
|
}
|
|
|
+ /* 与判卷 PDF(exam-grading / paper-body)一致的选项网格 */
|
|
|
+ .report-options {
|
|
|
+ margin-top: 6px;
|
|
|
+ page-break-inside: avoid;
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+ .report-options.options-grid-4 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 8px 12px;
|
|
|
+ }
|
|
|
+ .report-options.options-grid-2 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 8px 20px;
|
|
|
+ }
|
|
|
+ .report-options.options-grid-1 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ .report-options .option {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.6;
|
|
|
+ page-break-inside: avoid;
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+ .report-options .option strong { margin-right: 4px; flex: 0 0 auto; }
|
|
|
+ .report-options .option-value.option-short { white-space: nowrap; }
|
|
|
+ .report-options .option-value.option-long { white-space: normal; word-break: break-word; }
|
|
|
+ .report-options .option p, .report-options .option div { margin: 0; display: inline; }
|
|
|
+ .report-options .option img {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ vertical-align: middle;
|
|
|
+ }
|
|
|
+ /* 判卷风格:解题思路(与 answer-meta 一致) */
|
|
|
+ .report-answer-meta {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #2f2f2f;
|
|
|
+ line-height: 1.75;
|
|
|
+ margin-top: 6px;
|
|
|
+ page-break-inside: avoid;
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+ .report-answer-meta .answer-line + .answer-line { margin-top: 4px; }
|
|
|
+ .report-answer-meta .solution-content {
|
|
|
+ display: inline;
|
|
|
+ line-height: 1.75;
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -666,6 +764,9 @@
|
|
|
if ($analysis === null || $analysis === '') {
|
|
|
$analysis = '暂无解题思路,待补充';
|
|
|
}
|
|
|
+ if (is_string($analysis)) {
|
|
|
+ $analysis = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $analysis);
|
|
|
+ }
|
|
|
$formatSolutionLikeGrading = function ($text) {
|
|
|
if (!is_string($text) || trim($text) === '') {
|
|
|
return $text;
|
|
|
@@ -690,6 +791,9 @@
|
|
|
$typeLabel = $typeMap[$q['question_type'] ?? ''] ?? ($q['question_type'] ?? '题型未标注');
|
|
|
$questionText = is_string($q['question_text']) ? $q['question_text'] : json_encode($q['question_text'], JSON_UNESCAPED_UNICODE);
|
|
|
$solution = $q['solution'] ?? null;
|
|
|
+ if (is_string($solution)) {
|
|
|
+ $solution = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solution);
|
|
|
+ }
|
|
|
$solution = $formatSolutionLikeGrading($solution);
|
|
|
$analysis = $formatSolutionLikeGrading($analysis);
|
|
|
// 对齐判卷渲染口径:直接走 MathFormulaProcessor,保留图片与公式标签
|
|
|
@@ -703,6 +807,74 @@
|
|
|
}
|
|
|
return \App\Services\MathFormulaProcessor::processFormulas($text);
|
|
|
};
|
|
|
+ // 选择题:选项来自服务端 questions.options / 题干解析;正确答案字母用于打 ✅
|
|
|
+ $displayCorrectAnswer = is_array($correctAnswer)
|
|
|
+ ? json_encode($correctAnswer, JSON_UNESCAPED_UNICODE)
|
|
|
+ : (string) $correctAnswer;
|
|
|
+ $questionTypeRaw = strtolower(trim((string) ($q['question_type'] ?? '')));
|
|
|
+ $isChoiceQuestion = in_array($questionTypeRaw, ['choice', 'multiple_choice', 'single_choice', '选择题', 'select'], true);
|
|
|
+ $normalizedOptions = [];
|
|
|
+ $correctAnswerLetters = [];
|
|
|
+ if ($isChoiceQuestion) {
|
|
|
+ $rawOptions = $q['options'] ?? [];
|
|
|
+ if (is_string($rawOptions)) {
|
|
|
+ $decodedOptions = json_decode($rawOptions, true);
|
|
|
+ $rawOptions = is_array($decodedOptions) ? $decodedOptions : [];
|
|
|
+ }
|
|
|
+ if (is_array($rawOptions)) {
|
|
|
+ foreach ($rawOptions as $optKey => $optValue) {
|
|
|
+ $letter = null;
|
|
|
+ if (is_string($optKey) && preg_match('/([A-H])/i', $optKey, $m)) {
|
|
|
+ $letter = strtoupper($m[1]);
|
|
|
+ } elseif (is_array($optValue)) {
|
|
|
+ $candidate = $optValue['label'] ?? $optValue['key'] ?? $optValue['option'] ?? null;
|
|
|
+ if (is_string($candidate) && preg_match('/([A-H])/i', $candidate, $m)) {
|
|
|
+ $letter = strtoupper($m[1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($letter === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $content = is_array($optValue)
|
|
|
+ ? ($optValue['content'] ?? $optValue['text'] ?? $optValue['value'] ?? '')
|
|
|
+ : $optValue;
|
|
|
+ if (!is_string($content)) {
|
|
|
+ $content = json_encode($content, JSON_UNESCAPED_UNICODE);
|
|
|
+ }
|
|
|
+ $content = trim((string) $content);
|
|
|
+ if ($content !== '') {
|
|
|
+ $normalizedOptions[$letter] = $content;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (trim((string) $correctAnswer) !== '') {
|
|
|
+ preg_match_all('/[A-H]/i', strtoupper((string) $correctAnswer), $answerMatches);
|
|
|
+ $correctAnswerLetters = array_values(array_unique($answerMatches[0] ?? []));
|
|
|
+ }
|
|
|
+ if (!empty($normalizedOptions) && !empty($correctAnswerLetters)) {
|
|
|
+ $mappedAnswers = [];
|
|
|
+ foreach ($correctAnswerLetters as $letter) {
|
|
|
+ if (isset($normalizedOptions[$letter])) {
|
|
|
+ $mappedAnswers[] = $letter . '. ' . $normalizedOptions[$letter];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!empty($mappedAnswers)) {
|
|
|
+ $displayCorrectAnswer = implode(';', $mappedAnswers);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $choiceOptionLetters = !empty($normalizedOptions) ? array_keys($normalizedOptions) : [];
|
|
|
+ sort($choiceOptionLetters);
|
|
|
+ $choiceLayoutClass = 'options-grid-1';
|
|
|
+ $layoutDecider = app(\App\Support\OptionLayoutDecider::class);
|
|
|
+ if (! empty($normalizedOptions) && ! empty($choiceOptionLetters)) {
|
|
|
+ $optValuesForLayout = [];
|
|
|
+ foreach ($choiceOptionLetters as $L) {
|
|
|
+ $optValuesForLayout[] = $normalizedOptions[$L];
|
|
|
+ }
|
|
|
+ $layoutMeta = $layoutDecider->decide($optValuesForLayout, 'grading');
|
|
|
+ $choiceLayoutClass = $layoutMeta['class'] ?? 'options-grid-1';
|
|
|
+ }
|
|
|
@endphp
|
|
|
<div class="question-card">
|
|
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:4px;">
|
|
|
@@ -723,30 +895,60 @@
|
|
|
@endif
|
|
|
</div>
|
|
|
|
|
|
- <div class="math-content" style="margin-bottom:6px;">{!! $questionText !!}</div>
|
|
|
+ {{-- 题干:数据已在 ExamPdfExportService 中经 processQuestionData(含 <image>→<img>、公式);样式对齐判卷 question-stem --}}
|
|
|
+ <div class="question-stem math-content" style="margin-bottom:6px;">{!! $questionText !!}</div>
|
|
|
|
|
|
- {{-- 【修复】正确答案显示 --}}
|
|
|
- @if(!empty($correctAnswer))
|
|
|
+ {{-- 选择题:与判卷页相同网格布局(OptionLayoutDecider),正确项旁 ✅ --}}
|
|
|
+ @if(!empty($isChoiceQuestion) && !empty($normalizedOptions))
|
|
|
+ <div class="report-options {{ $choiceLayoutClass }}">
|
|
|
+ @foreach($choiceOptionLetters as $optLetter)
|
|
|
+ @php
|
|
|
+ $isCorrectOpt = in_array($optLetter, $correctAnswerLetters ?? [], true);
|
|
|
+ $rawOpt = (string) ($normalizedOptions[$optLetter] ?? '');
|
|
|
+ $normalizedOpt = str_replace('\\dfrac', '\\frac', $rawOpt);
|
|
|
+ $normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
|
|
|
+ $normalizedOpt = $layoutDecider->normalizeCompactMathForDisplay($normalizedOpt);
|
|
|
+ $rawOptPlain = html_entity_decode(strip_tags($rawOpt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
+ $rawOptPlain = preg_replace('/\s+/u', '', $rawOptPlain ?? '');
|
|
|
+ $isShortOption = mb_strlen((string) $rawOptPlain, 'UTF-8') <= 8;
|
|
|
+ $valClass = $isShortOption ? 'option-short' : 'option-long';
|
|
|
+ $renderedOpt = $renderLikeGrading($normalizedOpt);
|
|
|
+ @endphp
|
|
|
+ <div class="option option-compact">
|
|
|
+ <strong>{{ $optLetter }}.</strong>
|
|
|
+ <span class="option-value {{ $valClass }}">{!! $renderedOpt !!}</span>
|
|
|
+ @if($isCorrectOpt)
|
|
|
+ <span style="margin-left:4px; font-size:13px; color:#15803d; font-weight:700;">✅</span>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ {{-- 非选择题,或选择题未能得到选项列表时:显示「正确答案」文字框 --}}
|
|
|
+ @if(!empty($correctAnswer) && (!$isChoiceQuestion || empty($normalizedOptions)))
|
|
|
<div class="question-block" style="background:#f0fdf4; border-left:3px solid #10b981;">
|
|
|
<div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">正确答案</div>
|
|
|
<div class="math-content" style="line-height:1.7; color:#374151;">
|
|
|
- {!! is_string($correctAnswer) ? $correctAnswer : json_encode($correctAnswer, JSON_UNESCAPED_UNICODE) !!}
|
|
|
+ {!! $renderLikeGrading($displayCorrectAnswer) !!}
|
|
|
</div>
|
|
|
</div>
|
|
|
@endif
|
|
|
|
|
|
- {{-- 【修改】解题思路显示(优先显示solution,其次显示analysis) --}}
|
|
|
+ {{-- 解题思路:判卷页 answer-meta 口径(单行标题+正文,避免大块色条占行) --}}
|
|
|
@if(!empty($solution))
|
|
|
- <div class="question-block" style="margin-top:6px; background:#eff6ff; border-left:3px solid #3b82f6;">
|
|
|
- <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
|
|
|
- <div class="math-content solution-content" style="color:#374151;">
|
|
|
- {!! $renderLikeGrading($solution) !!}
|
|
|
+ <div class="report-answer-meta">
|
|
|
+ <div class="answer-line">
|
|
|
+ <strong>解题思路:</strong>
|
|
|
+ <span class="solution-content">{!! $renderLikeGrading($solution) !!}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
|
|
|
- <div class="question-block" style="margin-top:6px; background:#eff6ff; border-left:3px solid #3b82f6;">
|
|
|
- <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
|
|
|
- <div class="math-content solution-content" style="color:#374151;">{!! $renderLikeGrading($analysis) !!}</div>
|
|
|
+ <div class="report-answer-meta">
|
|
|
+ <div class="answer-line">
|
|
|
+ <strong>解题思路:</strong>
|
|
|
+ <span class="solution-content">{!! $renderLikeGrading($analysis) !!}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
@endif
|
|
|
|