knowledge-explanation-standalone.blade.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>数学知识点讲解</title>
  6. <link rel="stylesheet" href="/css/katex/katex.min.css">
  7. @include('pdf.partials.kp-explain-styles')
  8. <style>
  9. :root {
  10. --pdf-space-xxs: 2px;
  11. --pdf-space-xs: 4px;
  12. --pdf-space-sm: 6px;
  13. --pdf-space-md: 10px;
  14. --pdf-border-light: #d8d8d8;
  15. --pdf-border-strong: #777;
  16. --pdf-text-primary: #000;
  17. --pdf-text-secondary: #555;
  18. --pdf-radius-soft: 2px;
  19. --pdf-line-normal: 1.75;
  20. --pdf-line-relaxed: 1.9;
  21. }
  22. /* 与 exam-paper 一致:题干网格、选项网格、判卷答案区基础样式 */
  23. @include('pdf.partials.paper-body-core-styles')
  24. .case-list .option-compact { line-height: inherit; }
  25. .case-list .option .katex {
  26. font-size: 1em !important;
  27. vertical-align: 0;
  28. }
  29. .case-list .option .katex .mfrac {
  30. font-size: 1em !important;
  31. }
  32. .case-list .option .katex .mfrac .mtight {
  33. font-size: 1em !important;
  34. }
  35. .case-list .option .katex .frac-line {
  36. border-bottom-width: 0.055em !important;
  37. }
  38. .case-list .option .katex .mfrac .vlist > span:nth-child(1) {
  39. transform: translateY(0.24em) !important;
  40. }
  41. .case-list .option .katex .mfrac .vlist > span:nth-child(3) {
  42. transform: translateY(-0.16em) !important;
  43. }
  44. .case-list .option .katex-display {
  45. display: inline;
  46. margin: 0 !important;
  47. vertical-align: baseline;
  48. }
  49. .case-list .answer-meta {
  50. font-size: 12px;
  51. color: #2f2f2f;
  52. line-height: var(--pdf-line-normal);
  53. margin-top: var(--pdf-space-xs);
  54. page-break-inside: auto;
  55. break-inside: auto;
  56. }
  57. .case-list .answer-line + .answer-line { margin-top: var(--pdf-space-xs); }
  58. .case-list .solution-content { display: inline-block; line-height: var(--pdf-line-normal); }
  59. /* ========== 案例区整体 ========== */
  60. .case-list { margin-top: 14px; }
  61. .case-list .case-item {
  62. margin: 10px 0 14px;
  63. padding: 8px var(--pdf-space-md);
  64. border: none;
  65. border-left: 1.5px solid var(--pdf-border-light);
  66. border-radius: var(--pdf-radius-soft);
  67. background: #fff;
  68. line-height: var(--pdf-line-normal);
  69. break-inside: auto;
  70. page-break-inside: auto;
  71. orphans: 2;
  72. widows: 2;
  73. }
  74. .case-list .case-item + .case-item { margin-top: 9px; }
  75. /* ========== 题干区域 ========== */
  76. .case-list .kp-case-row {
  77. font-size: 14.5px;
  78. line-height: 1.8;
  79. margin-bottom: 3px;
  80. break-inside: auto;
  81. page-break-inside: auto;
  82. break-after: auto;
  83. page-break-after: auto;
  84. orphans: 2;
  85. widows: 2;
  86. }
  87. .case-list .kp-case-head-inline {
  88. display: inline;
  89. }
  90. .case-list .kp-case-prefix {
  91. font-weight: 700;
  92. white-space: nowrap;
  93. }
  94. .case-list .kp-case-title {
  95. font-size: 15px;
  96. font-weight: 700;
  97. color: var(--pdf-text-primary);
  98. margin-right: var(--pdf-space-xxs);
  99. white-space: nowrap;
  100. }
  101. .case-list .kp-case-source {
  102. font-size: 13px;
  103. color: var(--pdf-text-secondary);
  104. margin-right: 3px;
  105. font-weight: 700;
  106. }
  107. .case-list .kp-case-head-content {
  108. display: inline;
  109. }
  110. .case-list .kp-case-stem {
  111. font-size: 14.5px;
  112. font-weight: 400;
  113. line-height: 1.85;
  114. word-break: normal;
  115. overflow-wrap: break-word;
  116. break-inside: auto;
  117. page-break-inside: auto;
  118. orphans: 2;
  119. widows: 2;
  120. }
  121. /* ========== 解析区 ========== */
  122. .case-list .kp-case-meta-block { margin-top: 3px; line-height: 1.85; }
  123. .case-list .kp-case-meta-row {
  124. margin-top: 3px;
  125. text-indent: 0;
  126. page-break-inside: auto;
  127. break-inside: auto;
  128. }
  129. .case-list .kp-case-content {
  130. display: block;
  131. font-size: 14px;
  132. line-height: var(--pdf-line-relaxed);
  133. color: #222;
  134. white-space: normal;
  135. word-break: break-word;
  136. overflow-wrap: anywhere;
  137. orphans: 2;
  138. widows: 2;
  139. }
  140. .case-list .kp-case-answer-row {
  141. margin: var(--pdf-space-xs) 0 var(--pdf-space-sm);
  142. padding: 3px var(--pdf-space-sm);
  143. background: #f6f6f6;
  144. border-radius: var(--pdf-radius-soft);
  145. }
  146. .case-list .kp-case-solution-row .solution-content {
  147. display: block;
  148. margin-top: 2px;
  149. }
  150. /* 仅提升案例区 display 公式呼吸感,不动全局 KaTeX 行高 */
  151. .case-list .kp-case-content .katex-display,
  152. .case-list .kp-case-stem .katex-display {
  153. margin-top: 0.45em !important;
  154. margin-bottom: 0.5em !important;
  155. }
  156. .case-list .kp-case-content .complex-display-math,
  157. .case-list .kp-case-stem .complex-display-math {
  158. margin-top: 0.6em !important;
  159. margin-bottom: 0.65em !important;
  160. }
  161. /* ========== 小标题 ========== */
  162. .case-list .case-subtitle {
  163. display: block;
  164. font-weight: 700;
  165. font-size: 14px;
  166. margin-top: 8px;
  167. margin-bottom: var(--pdf-space-xxs);
  168. padding-left: 5px;
  169. border-left: 2px solid var(--pdf-border-strong);
  170. line-height: 1.35;
  171. color: var(--pdf-text-primary);
  172. break-after: avoid;
  173. page-break-after: avoid;
  174. }
  175. /* 标题+首段语义绑定:只对真正短小的纯文本节 avoid;含图/大公式自动分页,减少大面积空白 */
  176. .case-list .case-section {
  177. margin-top: var(--pdf-space-xs);
  178. break-inside: auto;
  179. page-break-inside: auto;
  180. }
  181. .case-list .case-section + .case-section { margin-top: 5px; }
  182. .case-list .case-section.case-section-keep {
  183. break-inside: avoid;
  184. page-break-inside: avoid;
  185. }
  186. .case-list .case-section.case-section-long {
  187. break-inside: auto;
  188. page-break-inside: auto;
  189. }
  190. .case-list .case-section-content { margin-top: 1px; }
  191. /* ========== 分页控制 ========== */
  192. .case-list .case-analysis {
  193. break-inside: auto;
  194. page-break-inside: auto;
  195. }
  196. .case-list .case-detail {
  197. break-inside: auto;
  198. page-break-inside: auto;
  199. }
  200. /* ========== 多小题 ========== */
  201. .case-list .sub-question {
  202. display: block;
  203. margin: 4px 0;
  204. padding-left: 2em;
  205. text-indent: -2em;
  206. line-height: 1.8;
  207. }
  208. /* 图片:与 exam-paper / 判卷一致 */
  209. @include('pdf.partials.paper-exam-shared-image-styles')
  210. /* 案例区优先节省纸张:不要让图和后续小题/答案强绑定到下一页 */
  211. .case-list .pdf-figure {
  212. break-inside: auto !important;
  213. page-break-inside: auto !important;
  214. -webkit-column-break-inside: auto !important;
  215. break-before: auto !important;
  216. break-after: auto !important;
  217. page-break-before: auto !important;
  218. page-break-after: auto !important;
  219. margin: 4px 0 !important;
  220. min-height: 0 !important;
  221. max-height: none !important;
  222. }
  223. .case-list .pdf-figure img,
  224. .case-list .kp-case-stem img {
  225. margin-top: 4px !important;
  226. margin-bottom: 4px !important;
  227. max-height: 48mm !important;
  228. }
  229. </style>
  230. </head>
  231. <body>
  232. <div class="page">
  233. <div class="kp-explain-header">
  234. <div class="kp-explain-title">数学知识点讲解</div>
  235. <div class="kp-explain-subtitle">围绕目标知识点生成讲解与案例,帮助学生高效复习。</div>
  236. </div>
  237. {{-- 知识点正文:与 pdf.exam-knowledge-explanation(知识点组卷前置梳理)同一套容器与数据(buildExplanations + normalizeKpExplanation) --}}
  238. @if(empty($knowledgePoints))
  239. <div class="kp-empty">暂无知识点数据</div>
  240. @else
  241. <div class="kp-list">
  242. @foreach($knowledgePoints as $point)
  243. <div class="kp-section">
  244. <div class="kp-section-head">
  245. <div class="kp-section-name">{{ $loop->iteration }}、{{ $point['kp_name'] ?? ($point['kp_code'] ?? '未命名知识点') }}</div>
  246. </div>
  247. <div class="kp-section-body">
  248. @if(!empty($point['explanation']))
  249. {!! $point['explanation'] !!}
  250. @endif
  251. </div>
  252. @if(!empty($point['cases']))
  253. @php
  254. // 与卷子题干一致的小题号断行逻辑(仅在至少2个小题编号时生效)
  255. $formatStemLikePaper = function (?string $text): string {
  256. $stem = trim((string) $text);
  257. if ($stem === '') {
  258. return '—';
  259. }
  260. preg_match_all('/[((][1-9][0-9]*[))]/u', $stem, $subQuestionMatches);
  261. $subQuestionCount = count($subQuestionMatches[0] ?? []);
  262. if ($subQuestionCount >= 2) {
  263. $stem = preg_replace('/^\s*([((][1-9][0-9]*[))])\s*/u', '$1 ', $stem) ?? $stem;
  264. // 断行后保留两个中文空格缩进
  265. $stem = preg_replace('/([。;;!?!?::.])\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>  $2 ', $stem) ?? $stem;
  266. $stem = preg_replace('/(?:(?:\\\\r\\\\n|\\\\n)|(?:\r?\n)|(?:<br\s*\/?>)|\s)+\s*([((][1-9][0-9]*[))])\s*/u', '<br>  $1 ', $stem) ?? $stem;
  267. $stem = preg_replace('/(求出|求解|求|写出|计算|证明|判断|化简)\s*([((][1-9][0-9]*[))])\s*/u', '$1<br>  $2 ', $stem) ?? $stem;
  268. }
  269. return $stem;
  270. };
  271. $markComplexDisplayMath = function (string $html): string {
  272. return preg_replace_callback(
  273. '/<span\b([^>]*\bclass="[^"]*\bkatex-display\b[^"]*"[^>]*)>(.*?)<\/span>/isu',
  274. static function (array $matches): string {
  275. $attrs = $matches[1] ?? '';
  276. $content = $matches[2] ?? '';
  277. $isComplex = str_contains($content, 'mfrac') || str_contains($content, 'vlist');
  278. if (!$isComplex || str_contains($attrs, 'complex-display-math')) {
  279. return $matches[0];
  280. }
  281. $attrs = preg_replace('/\bclass="([^"]*)"/u', 'class="$1 complex-display-math"', $attrs, 1) ?? $attrs;
  282. return '<span' . $attrs . '>' . $content . '</span>';
  283. },
  284. $html
  285. ) ?? $html;
  286. };
  287. // 解析:移除“讲解/解析”前缀,结构化分析/详解小标题,并保守处理步骤换行
  288. $formatSolution = function (?string $text) use ($markComplexDisplayMath): string {
  289. $solution = trim((string) $text);
  290. if ($solution === '') {
  291. return '—';
  292. }
  293. $solution = preg_replace('/^\s*[【\[]?\s*(讲解|解析)\s*[】\]]?\s*[::]\s*/u', '', $solution) ?? $solution;
  294. $solution = preg_replace('/\s*【\s*(分析|详解|点睛)\s*】\s*/u', '<div class="case-subtitle">$1</div>', $solution) ?? $solution;
  295. $solution = preg_replace('/\s*\[\s*(分析|详解|点睛)\s*\]\s*/u', '<div class="case-subtitle">$1</div>', $solution) ?? $solution;
  296. $solution = preg_replace('/\s*(步骤\s*[0-9一二三四五六七八九十百零两]+\s*[::]?)/u', '<br>$1', $solution) ?? $solution;
  297. $solution = preg_replace('/\s*(第\s*[0-9一二三四五六七八九十百零两]+\s*步\s*[::]?)/u', '<br>$1', $solution) ?? $solution;
  298. $plainLength = mb_strlen(strip_tags($solution), 'UTF-8');
  299. if ($plainLength > 160) {
  300. $solution = preg_replace('/([。;])\s*/u', '$1<br>', $solution) ?? $solution;
  301. }
  302. $solution = preg_replace('/(<\/div>)\s*<br>\s*(步骤\s*[0-9一二三四五六七八九十百零两]+\s*[::]?)/u', '$1$2', $solution) ?? $solution;
  303. $solution = preg_replace('/(<\/div>)\s*<br>\s*(第\s*[0-9一二三四五六七八九十百零两]+\s*步\s*[::]?)/u', '$1$2', $solution) ?? $solution;
  304. $solution = preg_replace('/^(?:\s*<br\s*\/?>\s*)+/iu', '', $solution) ?? $solution;
  305. $solution = preg_replace('/(?:<br>\s*){2,}/u', '<br>', $solution) ?? $solution;
  306. $solution = preg_replace('/(?:\s*<br>\s*)*(<div class="case-subtitle">)/u', '$1', $solution) ?? $solution;
  307. $solution = $markComplexDisplayMath($solution);
  308. // 将「分析/详解」转换为可分页语义块:标题 + 内容。
  309. // 仅短小纯文本节绑定;含图/display 公式的节允许自然分页,减少整块推页空白。
  310. $caseSectionClass = static function (string $body, bool $hasTitle = true): string {
  311. $plainLength = mb_strlen(trim(strip_tags($body)), 'UTF-8');
  312. $containsImage = str_contains($body, '<img') || str_contains($body, 'pdf-figure');
  313. $containsDisplayMath = str_contains($body, 'katex-display');
  314. $isShortSection = $plainLength < 120 && !$containsImage && !$containsDisplayMath;
  315. if ($hasTitle) {
  316. return $isShortSection ? 'case-section case-section-keep' : 'case-section case-section-long';
  317. }
  318. return $isShortSection ? 'case-section case-section-keep case-section-plain' : 'case-section case-section-long case-section-plain';
  319. };
  320. $chunks = preg_split('/<div class="case-subtitle">(.*?)<\/div>/u', $solution, -1, PREG_SPLIT_DELIM_CAPTURE);
  321. if (is_array($chunks) && count($chunks) > 1) {
  322. $sectionHtml = '';
  323. $lead = trim((string) ($chunks[0] ?? ''));
  324. if ($lead !== '') {
  325. $leadClass = $caseSectionClass($lead, false);
  326. $sectionHtml .= '<div class="' . $leadClass . '"><div class="case-section-content">' . $lead . '</div></div>';
  327. }
  328. for ($i = 1; $i < count($chunks); $i += 2) {
  329. $title = trim((string) ($chunks[$i] ?? ''));
  330. $body = trim((string) ($chunks[$i + 1] ?? ''));
  331. if ($title === '' && $body === '') {
  332. continue;
  333. }
  334. $sectionClass = $caseSectionClass($body, true);
  335. $sectionHtml .= '<div class="' . $sectionClass . '"><div class="case-subtitle">' . $title . '</div><div class="case-section-content">' . $body . '</div></div>';
  336. }
  337. if ($sectionHtml !== '') {
  338. $solution = $sectionHtml;
  339. }
  340. } else {
  341. $plainClass = $caseSectionClass($solution, false);
  342. $solution = '<div class="' . $plainClass . '"><div class="case-section-content">' . $solution . '</div></div>';
  343. }
  344. return $solution;
  345. };
  346. @endphp
  347. <div class="kp-markdown"><h3>案例分析</h3></div>
  348. <div class="case-list">
  349. @foreach($point['cases'] as $case)
  350. @php
  351. $sourceText = '';
  352. if (!empty($case['child_kp_name'])) {
  353. // 只展示子知识点来源,不展示父知识点名称
  354. $sourceText = trim((string) $case['child_kp_name']);
  355. $parentName = trim((string) ($point['kp_name'] ?? ''));
  356. if ($parentName !== '') {
  357. $escapedParent = preg_quote($parentName, '/');
  358. $sourceText = preg_replace('/^' . $escapedParent . '\s*[-—-\/]\s*/u', '', $sourceText) ?? $sourceText;
  359. }
  360. if (preg_match('/[-—-\/]/u', $sourceText)) {
  361. $parts = preg_split('/\s*[-—-\/]\s*/u', $sourceText);
  362. if (is_array($parts) && !empty($parts)) {
  363. $sourceText = trim((string) end($parts));
  364. }
  365. }
  366. } elseif (!empty($case['is_wrong_case'])) {
  367. $sourceText = '错题讲解';
  368. } elseif (($case['source_type'] ?? '') === 'reviewed') {
  369. $sourceText = '已做题';
  370. } elseif (($case['source_type'] ?? '') === 'fallback') {
  371. $sourceText = '补充题';
  372. }
  373. @endphp
  374. <div class="case-item">
  375. <div class="kp-case-row">
  376. @php
  377. $stemLine = trim((string) ($case['stem'] ?? ''));
  378. if ($stemLine === '') {
  379. $renderedStemHtml = '—';
  380. } else {
  381. [$renderedStemHtml] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan($stemLine, null, true, false);
  382. $renderedStemHtml = \App\Support\BlankPlaceholderRenderer::normalizeTerminalPunctuation($renderedStemHtml, 'remove');
  383. $renderedStemHtml = $formatStemLikePaper($renderedStemHtml);
  384. $renderedStemHtml = \App\Services\MathFormulaProcessor::processFormulas($renderedStemHtml);
  385. $renderedStemHtml = $markComplexDisplayMath($renderedStemHtml);
  386. }
  387. @endphp
  388. <div class="kp-case-head-inline">
  389. <span class="kp-case-prefix">
  390. <span class="kp-case-title">例{{ $loop->iteration }}.</span>
  391. @if($sourceText !== '')
  392. <span class="kp-case-source">({{ $sourceText }})</span>
  393. @endif
  394. </span>
  395. <span class="kp-case-head-content">
  396. <span class="kp-case-stem">{!! $renderedStemHtml !!}</span>
  397. </span>
  398. </div>
  399. @php
  400. $options = (array) ($case['options'] ?? []);
  401. @endphp
  402. @if(!empty($options))
  403. @include('pdf.partials.exam-choice-options', [
  404. 'options' => $options,
  405. 'gradingMode' => false,
  406. 'mathProcessed' => false,
  407. 'logQuestionNumber' => 'kp-' . ($case['question_id'] ?? $loop->iteration),
  408. 'showLeadSpacer' => false,
  409. ])
  410. @endif
  411. </div>
  412. <div class="kp-case-meta-block">
  413. @php
  414. $layoutDeciderService = app(\App\Support\OptionLayoutDecider::class);
  415. $choiceAnswerRaw = $layoutDeciderService->normalizeCompactMathForDisplay(trim((string) ($case['answer'] ?? '')));
  416. $solutionText = trim((string) ($case['solution'] ?? ''));
  417. $solutionText = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solutionText) ?? $solutionText;
  418. $solutionProcessed = $solutionText === ''
  419. ? ''
  420. : \App\Services\MathFormulaProcessor::processFormulas($solutionText);
  421. $solutionHtml = $solutionProcessed === ''
  422. ? '<span style="color:#999;font-style:italic;">(暂无解题思路)</span>'
  423. : $formatSolution($solutionProcessed);
  424. $answerLineHtml = $choiceAnswerRaw === ''
  425. ? '—'
  426. : \App\Services\MathFormulaProcessor::processFormulas($choiceAnswerRaw);
  427. @endphp
  428. <div class="answer-meta">
  429. <div class="answer-line kp-case-answer-row"><strong>正确答案:</strong><span class="solution-content">{!! $answerLineHtml !!}</span></div>
  430. <div class="answer-line kp-case-solution-row"><strong>解题思路:</strong><span class="solution-content kp-case-content">{!! $solutionHtml !!}</span></div>
  431. </div>
  432. </div>
  433. </div>
  434. @endforeach
  435. </div>
  436. @endif
  437. </div>
  438. @endforeach
  439. </div>
  440. @endif
  441. </div>
  442. </body>
  443. </html>