knowledge-explanation-standalone.blade.php 23 KB

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