pdf-report.blade.php 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. @php
  2. // 提取15位paper_id数字部分作为学案编号
  3. $rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
  4. preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
  5. $reportCode = $matches[1] ?? '';
  6. if ($reportCode === '') {
  7. $digits = preg_replace('/[^0-9]/', '', $rawPaperId);
  8. $reportCode = substr(str_pad($digits, 15, '0', STR_PAD_LEFT), -15);
  9. }
  10. $averageMastery = isset($mastery['average']) ? number_format($mastery['average'] * 100, 1) . '%' : '无数据';
  11. // 【修复】从insights中获取AI分析结果(而不是从analysis_data)
  12. $questionAnalysis = $insights ?? [];
  13. // 生成时间(格式:2026年01月30日 15:04:05)
  14. $generateDateTime = now()->format('Y年m月d日 H:i:s');
  15. @endphp
  16. <!DOCTYPE html>
  17. <html lang="zh-CN">
  18. <head>
  19. <meta charset="UTF-8">
  20. <title>学情报告 - {{ $paper['name'] ?? '试卷' }}</title>
  21. <link rel="stylesheet" href="/css/katex/katex.min.css">
  22. <style>
  23. @page {
  24. size: A4;
  25. margin: 2.2cm 2cm 2.3cm 2cm;
  26. @top-left {
  27. content: "知了数学·{{ $generateDateTime }}";
  28. font-size: 13px;
  29. color: #666;
  30. }
  31. @top-center {
  32. content: "{{ $student['name'] ?? '-' }}";
  33. font-size: 13px;
  34. color: #666;
  35. }
  36. @top-right {
  37. content: "{{ $reportCode }}";
  38. font-size: 19px;
  39. font-weight: 600;
  40. font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
  41. letter-spacing: 0;
  42. padding-right: 3mm;
  43. padding-top: 1.8mm;
  44. color: #222;
  45. }
  46. @bottom-left {
  47. content: "{{ $reportCode }}";
  48. font-size: 11px;
  49. color: #666;
  50. }
  51. @bottom-right {
  52. content: counter(page) "/" counter(pages);
  53. font-size: 13px;
  54. color: #666;
  55. }
  56. }
  57. * { box-sizing: border-box; }
  58. body {
  59. font-family: "SimSun", "Songti SC", serif;
  60. margin: 0;
  61. padding: 0;
  62. color: #000;
  63. background: #fff;
  64. font-size: 12px;
  65. line-height: 1.6;
  66. }
  67. h1, h2, h3 { margin: 0; color: #000; }
  68. .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); break-inside: auto; page-break-inside: auto; }
  69. .header { text-align: center; margin-bottom: 1.5rem; border-bottom: 2px solid #000; padding-bottom: 1rem; }
  70. .paper-title { font-size: 22px; font-weight: bold; margin-bottom: 14px; }
  71. .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
  72. .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; color: #374151; background: #e5e7eb; }
  73. .section-title { font-size: 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
  74. .pill { padding: 4px 10px; border-radius: 999px; font-size: 12px; }
  75. .pill.green { background: #ecfdf3; color: #15803d; }
  76. .pill.amber { background: #fef3c7; color: #b45309; }
  77. .pill.red { background: #fef2f2; color: #b91c1c; }
  78. table { width: 100%; border-collapse: collapse; font-size: 13px; }
  79. th, td { padding: 8px 10px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
  80. th { background: #f3f4f6; color: #111827; }
  81. .muted { color: #6b7280; font-size: 12px; }
  82. .progress-wrap { background: #f3f4f6; border-radius: 999px; overflow: hidden; height: 10px; }
  83. .progress-bar { height: 100%; background: linear-gradient(90deg, #4f46e5, #10b981); }
  84. .recommend-card { border: 1px dashed #cbd5e1; border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: #f8fafc; }
  85. .relation-board { margin: 6px 0 10px; break-inside: auto; page-break-inside: auto; }
  86. .relation-block { margin-bottom: 10px; padding: 8px; border: 1px solid #dbeafe; border-radius: 8px; background: #fff; break-inside: auto; page-break-inside: auto; }
  87. .relation-row { display: flex; align-items: flex-start; gap: 10px; }
  88. .tree-cards { width: 100%; border-collapse: collapse; table-layout: fixed; }
  89. .tree-cards td { border: none; padding: 0; vertical-align: top; }
  90. .tree-left { width: 45%; flex: 0 0 45%; padding-right: 10px; }
  91. .tree-right { width: 55%; flex: 1; padding-left: 10px; }
  92. .tree-parent {
  93. display: inline-block;
  94. margin-bottom: 6px;
  95. padding: 4px 8px;
  96. border-radius: 999px;
  97. border: 1px solid #93c5fd;
  98. background: #dbeafe;
  99. color: #1e3a8a;
  100. font-weight: 700;
  101. font-size: 12px;
  102. }
  103. .tree-lines { margin: 0; padding-left: 14px; border-left: 2px solid #cbd5e1; }
  104. .tree-line {
  105. font-size: 11px;
  106. color: #334155;
  107. margin-bottom: 3px;
  108. white-space: normal;
  109. overflow: visible;
  110. text-overflow: clip;
  111. word-break: break-all;
  112. line-height: 1.45;
  113. }
  114. .tree-line.hit { font-weight: 600; color: #1e40af; }
  115. .tree-line-badge { display: inline-block; margin-left: 6px; padding: 1px 5px; border-radius: 999px; font-size: 10px; border: 1px solid transparent; }
  116. .tree-line-badge.high,
  117. .tree-line-badge.mid,
  118. .tree-line-badge.low,
  119. .tree-line-badge.miss { background: #f8fafc; color: #64748b; border-color: #cbd5e1; }
  120. .detail-card {
  121. border: 1px solid #e2e8f0;
  122. border-radius: 6px;
  123. background: #f8fafc;
  124. padding: 6px 8px;
  125. margin-bottom: 5px;
  126. break-inside: auto;
  127. page-break-inside: auto;
  128. }
  129. .detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
  130. .detail-title { font-size: 11px; color: #0f172a; font-weight: 700; margin-bottom: 2px; }
  131. .detail-meta { font-size: 11px; color: #475569; margin-top: 3px; }
  132. .detail-mastery { font-size: 38px; color: #0f172a; font-weight: 700; line-height: 1; margin: 0; }
  133. .detail-mastery.high { color: #16a34a; }
  134. .detail-mastery.mid { color: #d97706; }
  135. .detail-mastery.low { color: #dc2626; }
  136. .node-tags { margin-top: 2px; }
  137. .node-tag {
  138. display: inline-block;
  139. margin: 0 6px 6px 0;
  140. padding: 1px 7px;
  141. border-radius: 999px;
  142. font-size: 10px;
  143. line-height: 1.4;
  144. color: #475569;
  145. background: #f1f5f9;
  146. border: 1px solid #cbd5e1;
  147. }
  148. .suggest-tags { display: inline; }
  149. .suggest-tag {
  150. display: inline-block;
  151. margin: 0 6px 6px 0;
  152. padding: 1px 7px;
  153. border-radius: 999px;
  154. font-size: 10px;
  155. line-height: 1.4;
  156. color: #334155;
  157. background: #f8fafc;
  158. border: 1px solid #d1d5db;
  159. }
  160. .weak-kp-tags { margin-top: 6px; }
  161. .weak-kp-tag {
  162. display: inline-block;
  163. margin: 0 6px 6px 0;
  164. padding: 1px 7px;
  165. border-radius: 999px;
  166. font-size: 10px;
  167. line-height: 1.45;
  168. color: #854d0e;
  169. background: #fef3c7;
  170. border: 1px solid #fcd34d;
  171. }
  172. .error-kp-tag {
  173. display: inline-block;
  174. margin: 0 6px 6px 0;
  175. padding: 1px 7px;
  176. border-radius: 999px;
  177. font-size: 10px;
  178. line-height: 1.45;
  179. color: #334155;
  180. background: #f8fafc;
  181. border: 1px solid #d1d5db;
  182. }
  183. .error-kp-tag.high-risk {
  184. color: #b91c1c;
  185. border-color: #fca5a5;
  186. background: #fff;
  187. font-weight: 600;
  188. }
  189. .detail-change-up { color: #16a34a; font-weight: 700; }
  190. .detail-change-down { color: #dc2626; font-weight: 700; }
  191. .aggregate-tip { font-size: 11px; color: #475569; margin-top: 6px; }
  192. .question-card { border:1px solid #e5e7eb; border-radius:8px; padding:6px 9px; margin-bottom:5px; background:#fff; page-break-inside: auto; break-inside: auto; }
  193. .question-block { margin-bottom: 5px; padding: 5px; border-radius: 4px; page-break-inside: auto; break-inside: auto; }
  194. .question-card,
  195. .question-card .math-content,
  196. .question-card .solution-content {
  197. font-size: 12px;
  198. line-height: 1.7;
  199. }
  200. /* 题干区:与判卷 PDF(exam-grading)一致 — <img>、内嵌 SVG、公式 */
  201. .question-card .question-stem svg,
  202. .question-card .math-content svg {
  203. max-width: 100%;
  204. height: auto;
  205. display: block;
  206. shape-rendering: geometricPrecision;
  207. text-rendering: geometricPrecision;
  208. }
  209. .question-card .question-stem svg text {
  210. font-family: "Noto Serif", "Noto Serif CJK SC", "Noto Sans CJK SC", "Noto Sans", "STSongti-SC", "PingFang SC", "Songti SC", serif !important;
  211. font-size: 13px !important;
  212. font-weight: bold;
  213. dominant-baseline: middle;
  214. text-anchor: middle;
  215. }
  216. .question-card .question-stem svg circle,
  217. .question-card .question-stem svg line,
  218. .question-card .question-stem svg polygon,
  219. .question-card .question-stem svg polyline {
  220. shape-rendering: geometricPrecision;
  221. }
  222. .question-card .question-stem img,
  223. .question-card .question-main img {
  224. display: block;
  225. max-width: 220px;
  226. max-height: 60mm;
  227. width: auto;
  228. height: auto;
  229. margin: 6px auto;
  230. box-sizing: border-box;
  231. object-fit: contain;
  232. -webkit-print-color-adjust: exact;
  233. print-color-adjust: exact;
  234. image-rendering: -webkit-optimize-contrast;
  235. }
  236. .question-card .question-stem .katex {
  237. font-size: 1em !important;
  238. vertical-align: 0;
  239. }
  240. .question-card .question-stem .katex-display {
  241. margin: 0.35em 0 !important;
  242. }
  243. .question-card .solution-content img,
  244. .question-card .report-answer-meta img {
  245. display: block;
  246. max-width: 220px;
  247. max-height: 60mm;
  248. width: auto;
  249. height: auto;
  250. margin: 6px auto;
  251. object-fit: contain;
  252. -webkit-print-color-adjust: exact;
  253. print-color-adjust: exact;
  254. }
  255. .solution-content {
  256. display: block;
  257. line-height: 1.75;
  258. white-space: normal;
  259. word-break: break-word;
  260. overflow-wrap: anywhere;
  261. page-break-inside: auto;
  262. break-inside: auto;
  263. }
  264. /* 与判卷 PDF(exam-grading / paper-body)一致的选项网格 */
  265. .report-options {
  266. margin-top: 6px;
  267. page-break-inside: avoid;
  268. break-inside: avoid;
  269. }
  270. .report-options.options-grid-4 {
  271. display: grid;
  272. grid-template-columns: repeat(4, 1fr);
  273. gap: 8px 12px;
  274. }
  275. .report-options.options-grid-2 {
  276. display: grid;
  277. grid-template-columns: 1fr 1fr;
  278. gap: 8px 20px;
  279. }
  280. .report-options.options-grid-1 {
  281. display: grid;
  282. grid-template-columns: 1fr;
  283. gap: 8px;
  284. }
  285. .report-options .option {
  286. display: flex;
  287. align-items: baseline;
  288. font-size: 12px;
  289. line-height: 1.6;
  290. page-break-inside: avoid;
  291. break-inside: avoid;
  292. }
  293. .report-options .option strong { margin-right: 4px; flex: 0 0 auto; }
  294. .report-options .option-value.option-short { white-space: nowrap; }
  295. .report-options .option-value.option-long { white-space: normal; word-break: break-word; }
  296. .report-options .option p, .report-options .option div { margin: 0; display: inline; }
  297. .report-options .option img {
  298. max-width: 100%;
  299. height: auto;
  300. vertical-align: middle;
  301. }
  302. /* 判卷风格:解题思路(与 answer-meta 一致) */
  303. .report-answer-meta {
  304. font-size: 12px;
  305. color: #2f2f2f;
  306. line-height: 1.75;
  307. margin-top: 6px;
  308. page-break-inside: avoid;
  309. break-inside: avoid;
  310. }
  311. .report-answer-meta .answer-line + .answer-line { margin-top: 4px; }
  312. .report-answer-meta .solution-content {
  313. display: inline;
  314. line-height: 1.75;
  315. }
  316. </style>
  317. </head>
  318. <body>
  319. <div class="page">
  320. <div class="header">
  321. <h1 class="paper-title">学情分析报告</h1>
  322. @php
  323. $teacherName = trim((string) ($teacher['name'] ?? ''));
  324. $showTeacher = $teacherName !== '' && $teacherName !== '________' && $teacherName !== '未知老师';
  325. @endphp
  326. <div style="display:flex;justify-content:space-between;font-size:14px;margin-top:8px;">
  327. @if($showTeacher)
  328. <span>老师:{{ $teacherName }}</span>
  329. @endif
  330. <span>年级:@formatGrade($student['grade'] ?? '________')</span>
  331. @if(!empty($paper['assemble_type_label']) && $paper['assemble_type_label'] !== '未知类型')
  332. <span>类型:{{ $paper['assemble_type_label'] }}</span>
  333. @endif
  334. <span>姓名:{{ $student['name'] ?? '________' }}</span>
  335. <span>题目数:{{ $paper['total_questions'] ?? (is_array($questions ?? null) ? count($questions) : '-') }}</span>
  336. </div>
  337. </div>
  338. <div class="card">
  339. <div class="section-title">本次命中子知识点掌握度</div>
  340. @php
  341. // 严格口径:顶部列表来自“本卷题目关联知识点”
  342. $hitCodes = array_values(array_unique(array_filter($exam_hit_kp_codes ?? [])));
  343. $masteryByCode = [];
  344. $hitMasteryFromParents = [];
  345. $questionKpNameMap = [];
  346. foreach (($questions ?? []) as $q) {
  347. $code = trim((string) ($q['knowledge_point'] ?? ''));
  348. $name = trim((string) ($q['knowledge_point_name'] ?? ''));
  349. if ($code !== '' && $name !== '') {
  350. $questionKpNameMap[$code] = $name;
  351. }
  352. }
  353. // 优先使用父节点树里的命中子节点掌握度(与左侧树口径一致)
  354. foreach (($parent_mastery_levels ?? []) as $parent) {
  355. $childrenAll = $parent['children_all'] ?? [];
  356. if (!is_array($childrenAll)) {
  357. continue;
  358. }
  359. foreach ($childrenAll as $child) {
  360. $cCode = trim((string) ($child['kp_code'] ?? ''));
  361. if ($cCode === '' || empty($child['is_hit'])) {
  362. continue;
  363. }
  364. $hitMasteryFromParents[$cCode] = floatval($child['mastery_level'] ?? 0);
  365. }
  366. }
  367. foreach (($mastery['items'] ?? []) as $item) {
  368. $code = (string) ($item['kp_code'] ?? '');
  369. if ($code !== '') {
  370. $masteryByCode[$code] = $item;
  371. }
  372. }
  373. $filteredMasteryItems = [];
  374. foreach ($hitCodes as $kpCode) {
  375. if (in_array($kpCode, ['K-GENERAL', 'GENERAL', 'DEFAULT'])) {
  376. continue;
  377. }
  378. $base = $masteryByCode[$kpCode] ?? null;
  379. $levelFromParentTree = $hitMasteryFromParents[$kpCode] ?? null;
  380. $resolvedLevel = $levelFromParentTree !== null
  381. ? floatval($levelFromParentTree)
  382. : floatval($base['mastery_level'] ?? 0);
  383. $resolvedChange = $base['mastery_change'] ?? null;
  384. // 当顶部使用了父节点树中的实时值且与旧值不一致时,避免显示旧变化值误导
  385. if ($levelFromParentTree !== null && $base !== null) {
  386. $oldLevel = floatval($base['mastery_level'] ?? 0);
  387. if (abs($resolvedLevel - $oldLevel) > 0.0001) {
  388. $resolvedChange = null;
  389. }
  390. }
  391. $filteredMasteryItems[] = [
  392. 'kp_code' => $kpCode,
  393. 'kp_name' => $questionKpNameMap[$kpCode] ?? ($base['kp_name'] ?? $kpCode),
  394. 'mastery_level' => $resolvedLevel,
  395. 'mastery_change' => $resolvedChange,
  396. 'is_hit' => true,
  397. ];
  398. }
  399. @endphp
  400. @if(!empty($filteredMasteryItems))
  401. @foreach($filteredMasteryItems as $item)
  402. @php
  403. $pct = min(100, max(0, ($item['mastery_level'] ?? 0) * 100));
  404. $barColor = $pct >= 80 ? '#10b981' : ($pct >= 60 ? '#f59e0b' : '#ef4444');
  405. $delta = $item['mastery_change'] ?? null;
  406. $isHitItem = !empty($item['is_hit']);
  407. // 只有当有变化值时才显示变化信息
  408. $changeText = '';
  409. if ($delta !== null && $delta !== '' && abs($delta) > 0.001) {
  410. $changeText = ($delta > 0 ? '↑ ' : '↓ ') . number_format(abs($delta) * 100, 1) . '%';
  411. }
  412. @endphp
  413. <div style="margin-bottom:12px; padding: 8px; border: 1px solid #e5e7eb; border-radius: 6px;">
  414. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 6px;">
  415. <div style="font-weight: 600; font-size: 13px;">{{ $item['kp_name'] ?? $item['kp_code'] ?? '未知知识点' }}</div>
  416. <div style="font-weight: 600; color: {{ $barColor }}; font-size: 13px;">
  417. {{ number_format($pct, 1) }}%
  418. <span style="margin-left: 8px; color: #666; font-size: 11px;">{{ $changeText ? '子节点变化 ' . $changeText : '' }}</span>
  419. </div>
  420. </div>
  421. <div class="progress-wrap" style="height: 12px;">
  422. <div class="progress-bar" style="width: {{ $pct }}%; background: {{ $barColor }};"></div>
  423. </div>
  424. </div>
  425. @endforeach
  426. @else
  427. <div class="muted">暂无有效掌握度数据(已过滤通用知识点)</div>
  428. @endif
  429. </div>
  430. <div class="card">
  431. <div class="section-title">📊 知识点层级掌握度分析</div>
  432. @php
  433. $parentMasteryLevels = $parent_mastery_levels ?? [];
  434. $hasParentMastery = !empty($parentMasteryLevels);
  435. $childMasteryMap = [];
  436. foreach (($filteredMasteryItems ?? []) as $it) {
  437. $childMasteryMap[$it['kp_code']] = [
  438. 'level' => floatval($it['mastery_level'] ?? 0),
  439. 'delta' => isset($it['mastery_change']) ? floatval($it['mastery_change']) : null,
  440. ];
  441. }
  442. @endphp
  443. @if($hasParentMastery)
  444. <div class="relation-board">
  445. <div style="font-size:12px; color:#334155; margin-bottom:6px; font-weight:600;">父子知识点关系</div>
  446. @foreach($parentMasteryLevels as $parentData)
  447. @php
  448. $childrenAll = $parentData['children_all'] ?? [];
  449. $children = $parentData['children'] ?? []; // 命中子节点
  450. $childCount = count($childrenAll);
  451. $hitCount = count($children);
  452. $hitAvg = $parentData['children_hit_avg_mastery'] ?? null;
  453. $parentPct = number_format(floatval($parentData['mastery_percentage'] ?? 0), 1);
  454. $parentLevel = floatval($parentData['mastery_level'] ?? 0);
  455. $parentClass = $parentLevel >= 0.8 ? 'high' : ($parentLevel >= 0.6 ? 'mid' : 'low');
  456. $delta = $parentData['mastery_change'] ?? null;
  457. $hitNames = array_values(array_map(fn($c) => $c['kp_name'] ?? ($c['kp_code'] ?? ''), $children));
  458. $allChildrenPerfect = !empty($childrenAll) && count(array_filter($childrenAll, function ($c) {
  459. return floatval($c['mastery_level'] ?? 0) >= 0.999;
  460. })) === count($childrenAll);
  461. @endphp
  462. <div class="relation-block">
  463. <div class="relation-row">
  464. <div class="tree-left">
  465. <div class="tree-parent">{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}</div>
  466. @if(!empty($childrenAll))
  467. <div class="tree-lines">
  468. @foreach($childrenAll as $child)
  469. @php
  470. $isHit = !empty($child['is_hit']);
  471. $m = floatval($child['mastery_level'] ?? 0);
  472. $badgeClass = $m >= 0.8 ? 'high' : ($m >= 0.6 ? 'mid' : ($m > 0 ? 'low' : 'miss'));
  473. $badgeText = number_format($m * 100, 1) . '%';
  474. @endphp
  475. <div class="tree-line {{ $isHit ? 'hit' : '' }}">
  476. └─ {{ $child['kp_name'] }}
  477. <span class="tree-line-badge {{ $badgeClass }}">{{ $badgeText }}</span>
  478. </div>
  479. @endforeach
  480. </div>
  481. @else
  482. <div class="muted">无命中子知识点</div>
  483. @endif
  484. </div>
  485. <div class="tree-right">
  486. <div class="detail-card">
  487. <div class="detail-head">
  488. <div class="detail-title">{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}</div>
  489. <div class="detail-mastery {{ $parentClass }}">{{ $parentPct }}%</div>
  490. </div>
  491. <div class="detail-meta">
  492. 父节点变化:
  493. @if($delta !== null)
  494. <span class="{{ $delta >= 0 ? 'detail-change-up' : 'detail-change-down' }}">
  495. {{ $delta >= 0 ? '↑ ' : '↓ ' }}{{ number_format(abs($delta) * 100, 1) }}%
  496. </span>
  497. @else
  498. -
  499. @endif
  500. </div>
  501. <div class="detail-meta" style="margin-top: 3px;">
  502. 本次重点子节点:
  503. @if($hitCount > 0)
  504. <span class="node-tags">
  505. @foreach($hitNames as $hitName)
  506. @if(!empty(trim($hitName)))
  507. <span class="node-tag">{{ $hitName }}</span>
  508. @endif
  509. @endforeach
  510. </span>
  511. @else
  512. @endif
  513. </div>
  514. <div class="detail-meta">
  515. 子节点总数 {{ $childCount }} 个,本次命中 {{ $hitCount }} 个,命中均值 {{ $hitAvg !== null ? number_format(floatval($hitAvg) * 100, 1) . '%' : '-' }}(命中表示本次覆盖,不等于已掌握)
  516. </div>
  517. </div>
  518. </div>
  519. </div>
  520. </div>
  521. @endforeach
  522. </div>
  523. @php
  524. $allParentPerfect = !empty($parentMasteryLevels) && count(array_filter($parentMasteryLevels, function ($p) {
  525. return floatval($p['mastery_level'] ?? 0) >= 0.999;
  526. })) === count($parentMasteryLevels);
  527. $allHitChildrenPerfect = !empty($filteredMasteryItems) && count(array_filter($filteredMasteryItems, function ($it) {
  528. return floatval($it['mastery_level'] ?? 0) >= 0.999;
  529. })) === count($filteredMasteryItems);
  530. $isAllPerfect = $allParentPerfect && $allHitChildrenPerfect;
  531. // 1) 优先:本次命中子知识点中的低掌握度(<60%)
  532. $hitWeakChildren = [];
  533. $hitWeakKeys = [];
  534. foreach (($filteredMasteryItems ?? []) as $hitItem) {
  535. $level = floatval($hitItem['mastery_level'] ?? 0);
  536. $name = trim((string) ($hitItem['kp_name'] ?? $hitItem['kp_code'] ?? ''));
  537. if ($name === '' || $level >= 0.6) {
  538. continue;
  539. }
  540. $key = (string) ($hitItem['kp_code'] ?? $name);
  541. $hitWeakChildren[$key] = [
  542. 'name' => $name,
  543. 'level' => $level,
  544. ];
  545. $hitWeakKeys[$key] = true;
  546. }
  547. $hitWeakChildren = array_values($hitWeakChildren);
  548. usort($hitWeakChildren, function ($a, $b) {
  549. return $a['level'] <=> $b['level'];
  550. });
  551. // 2) 兜底:若命中子知识点都 >=60%,再从其他低掌握度子知识点补
  552. $otherWeakChildren = [];
  553. foreach (($parentMasteryLevels ?? []) as $pData) {
  554. foreach (($pData['children_all'] ?? []) as $child) {
  555. $level = floatval($child['mastery_level'] ?? 0);
  556. $name = trim((string) ($child['kp_name'] ?? $child['kp_code'] ?? ''));
  557. if ($name === '' || $level >= 0.6) {
  558. continue;
  559. }
  560. $key = ($child['kp_code'] ?? $name);
  561. if (isset($hitWeakKeys[$key])) {
  562. continue;
  563. }
  564. $otherWeakChildren[$key] = [
  565. 'name' => $name,
  566. 'level' => $level,
  567. ];
  568. }
  569. }
  570. $otherWeakChildren = array_values($otherWeakChildren);
  571. usort($otherWeakChildren, function ($a, $b) {
  572. return $a['level'] <=> $b['level'];
  573. });
  574. // 3) 最终展示:总数不超过5
  575. $globalWeakChildren = [];
  576. if (!empty($hitWeakChildren)) {
  577. $globalWeakChildren = array_slice($hitWeakChildren, 0, 5);
  578. } else {
  579. $globalWeakChildren = array_slice($otherWeakChildren, 0, 5);
  580. }
  581. @endphp
  582. <div style="margin-top: 12px; padding: 8px; background: #fefce8; border-left: 4px solid #eab308; border-radius: 4px;">
  583. <div style="font-size: 12px; color: #854d0e; line-height: 1.6;">
  584. <strong>学习建议:</strong>
  585. @if($isAllPerfect)
  586. 本次学案表现非常出色,相关知识点掌握稳定且完整。建议继续进入新的知识点专题学习,优先选择同层级未覆盖内容或更高难度综合题,保持进阶节奏。
  587. @else
  588. 建议重点关注掌握度较低的知识点,通过专项练习提升整体学习水平。优先练习掌握度低于60%的知识点。
  589. @endif
  590. @if(!empty($globalWeakChildren))
  591. <div class="weak-kp-tags">
  592. @foreach($globalWeakChildren as $weakChild)
  593. <span class="weak-kp-tag">{{ $weakChild['name'] }}({{ number_format($weakChild['level'] * 100, 1) }}%)</span>
  594. @endforeach
  595. </div>
  596. @endif
  597. </div>
  598. </div>
  599. @else
  600. <div class="muted">暂无父节点掌握度数据</div>
  601. <div style="margin-top: 8px; font-size: 12px; color: #64748b;">
  602. 当前分析主要基于具体知识点掌握度。完整的层级掌握度分析需要在系统中建立完整的知识点层级关系。
  603. </div>
  604. @endif
  605. </div>
  606. @php
  607. $insightMap = [];
  608. foreach (($question_insights ?? []) as $insight) {
  609. $no = $insight['question_number'] ?? $insight['question_id'] ?? null;
  610. if ($no !== null) {
  611. $insightMap[$no] = $insight;
  612. }
  613. }
  614. $analysisWrongMap = [];
  615. foreach (($analysis_data['question_analysis'] ?? []) as $qa) {
  616. $qid = $qa['question_bank_id'] ?? $qa['question_id'] ?? null;
  617. if ($qid === null || $qid === '') {
  618. continue;
  619. }
  620. $rawCorrect = $qa['is_correct'] ?? null;
  621. $isWrongFromAnalysis = false;
  622. if (is_array($rawCorrect)) {
  623. $isWrongFromAnalysis = in_array(0, $rawCorrect, true);
  624. } elseif ($rawCorrect !== null) {
  625. $isWrongFromAnalysis = !boolval($rawCorrect);
  626. }
  627. if ($isWrongFromAnalysis) {
  628. $analysisWrongMap[(string)$qid] = true;
  629. }
  630. }
  631. $wrongQuestions = [];
  632. foreach (($questions ?? []) as $qItem) {
  633. $studentAnswerProbe = $qItem['student_answer'] ?? null;
  634. $correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? null);
  635. $isCorrectProbe = $qItem['is_correct'] ?? null;
  636. if ($isCorrectProbe === null && !empty($studentAnswerProbe) && !empty($correctAnswerProbe)) {
  637. $isCorrectProbe = (trim((string)$studentAnswerProbe) === trim((string)$correctAnswerProbe)) ? 1 : 0;
  638. }
  639. $normalizedCorrect = $isCorrectProbe;
  640. if ($isCorrectProbe !== null) {
  641. $normalizedCorrect = is_bool($isCorrectProbe) ? ($isCorrectProbe ? 1 : 0) : intval($isCorrectProbe);
  642. }
  643. $qidProbe = (string)($qItem['question_bank_id'] ?? $qItem['question_id'] ?? '');
  644. $isWrongByAnalysis = ($qidProbe !== '' && isset($analysisWrongMap[$qidProbe]));
  645. if ($normalizedCorrect === 0 || $isWrongByAnalysis) {
  646. $wrongQuestions[] = $qItem;
  647. }
  648. }
  649. // 错题知识点聚合统计(同知识点错几题/共几题)
  650. $kpStats = [];
  651. foreach (($questions ?? []) as $qItem) {
  652. $kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  653. if ($kpName === '') {
  654. $kpName = '未标注知识点';
  655. }
  656. if (!isset($kpStats[$kpName])) {
  657. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  658. }
  659. $kpStats[$kpName]['total']++;
  660. }
  661. foreach ($wrongQuestions as $qItem) {
  662. $kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  663. if ($kpName === '') {
  664. $kpName = '未标注知识点';
  665. }
  666. if (!isset($kpStats[$kpName])) {
  667. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  668. }
  669. $kpStats[$kpName]['wrong']++;
  670. }
  671. $kpWrongStats = [];
  672. foreach ($kpStats as $kpName => $stat) {
  673. if (($stat['wrong'] ?? 0) <= 0) {
  674. continue;
  675. }
  676. $total = max(1, intval($stat['total'] ?? 0));
  677. $wrong = intval($stat['wrong'] ?? 0);
  678. $kpWrongStats[] = [
  679. 'kp_name' => $kpName,
  680. 'wrong' => $wrong,
  681. 'total' => $total,
  682. 'rate' => $wrong / $total,
  683. ];
  684. }
  685. usort($kpWrongStats, function ($a, $b) {
  686. if ($a['rate'] === $b['rate']) {
  687. return $b['wrong'] <=> $a['wrong'];
  688. }
  689. return $b['rate'] <=> $a['rate'];
  690. });
  691. @endphp
  692. @if(!empty($wrongQuestions))
  693. <div class="card">
  694. <div class="section-title">错题列表</div>
  695. @if(!empty($kpWrongStats))
  696. <div style="margin-bottom:8px; padding:8px; border:1px solid #e5e7eb; border-radius:6px; background:#f8fafc;">
  697. <div style="font-size:12px; font-weight:600; color:#111827; margin-bottom:6px;">知识点错误率</div>
  698. <div style="font-size:12px; color:#475569; line-height:1.7;">
  699. @foreach($kpWrongStats as $item)
  700. <span class="error-kp-tag {{ $item['rate'] > 0.5 ? 'high-risk' : '' }}">{{ $item['kp_name'] }}:{{ $item['wrong'] }}/{{ $item['total'] }}({{ number_format($item['rate'] * 100, 1) }}%)</span>
  701. @endforeach
  702. </div>
  703. </div>
  704. @endif
  705. @foreach($wrongQuestions as $q)
  706. @php
  707. // 【修复】从题目数据中获取学生答案、正确答案和判分结果
  708. $studentAnswer = $q['student_answer'] ?? $q['student_answer'] ?? null;
  709. $correctAnswer = $q['answer'] ?? $q['correct_answer'] ?? null;
  710. $isCorrect = $q['is_correct'] ?? null; // 1=正确,0=错误,null=未判
  711. // 如果未判分但有学生答案和正确答案,自动比较判断
  712. if ($isCorrect === null && !empty($studentAnswer) && !empty($correctAnswer)) {
  713. $isCorrect = (trim($studentAnswer) === trim($correctAnswer)) ? 1 : 0;
  714. }
  715. // 判分状态显示逻辑
  716. $statusText = '';
  717. $statusColor = '';
  718. if ($isCorrect === 1) {
  719. $statusText = '正确';
  720. $statusColor = '#10b981';
  721. } elseif ($isCorrect === 0) {
  722. $statusText = '错误';
  723. $statusColor = '#ef4444';
  724. }
  725. $showStatus = $statusText !== '';
  726. $insight = $insightMap[$q['question_number']] ?? ($insightMap[$q['display_number'] ?? null] ?? []);
  727. // 【修复】得分显示:答错显示实际得分,答对显示满分
  728. $fullScore = $insight['full_score'] ?? ($q['score'] ?? null);
  729. if ($isCorrect === 1) {
  730. // 答对了,显示满分
  731. $score = $fullScore;
  732. } elseif ($isCorrect === 0) {
  733. // 答错了,显示实际得分(可能为0分或其他分数)
  734. $score = $q['score_obtained'] ?? 0;
  735. } else {
  736. // 未判分或未答题,不显示得分
  737. $score = null;
  738. }
  739. $analysisRaw = $insight['analysis']
  740. ?? $insight['thinking_process']
  741. ?? $insight['feedback']
  742. ?? $insight['suggestions']
  743. ?? $insight['reason']
  744. ?? ($insight['correct_solution'] ?? null);
  745. // 若有下一步建议,追加
  746. if (empty($analysisRaw) && !empty($insight['next_steps'])) {
  747. $analysisRaw = '后续建议:' . (is_array($insight['next_steps']) ? implode(';', $insight['next_steps']) : $insight['next_steps']);
  748. }
  749. $analysis = is_array($analysisRaw) ? json_encode($analysisRaw, JSON_UNESCAPED_UNICODE) : $analysisRaw;
  750. if ($analysis === null || $analysis === '') {
  751. $analysis = '暂无解题思路,待补充';
  752. }
  753. if (is_string($analysis)) {
  754. $analysis = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $analysis);
  755. }
  756. $formatSolutionLikeGrading = function ($text) {
  757. if (!is_string($text) || trim($text) === '') {
  758. return $text;
  759. }
  760. $normalized = preg_replace('/\s*;\s*步骤\s*(\d+)/u', ";\n步骤$1", $text);
  761. $normalized = preg_replace('/\s*。\s*步骤\s*(\d+)/u', "。\n步骤$1", $normalized);
  762. $normalized = preg_replace('/(?<!^)(步骤\s*\d+\s*[::])/u', "\n$1", $normalized);
  763. $normalized = preg_replace('/(?<!^)(第\s*\d+\s*步\s*[::]?)/u', "\n$1", $normalized);
  764. $normalized = preg_replace('/\n{3,}/u', "\n\n", $normalized);
  765. // 去掉每行左侧缩进空白,避免出现“左边空好几个字符”
  766. $normalized = preg_replace('/^[\h\x{3000}]+/mu', '', $normalized);
  767. return trim($normalized);
  768. };
  769. $stepsRaw = $insight['steps'] ?? $insight['solution_steps'] ?? $insight['analysis_steps'] ?? null;
  770. $steps = [];
  771. if (is_array($stepsRaw)) {
  772. $steps = $stepsRaw;
  773. } elseif (is_string($stepsRaw) && trim($stepsRaw) !== '') {
  774. $steps = preg_split('/[\r\n]+/', trim($stepsRaw));
  775. }
  776. $typeMap = ['choice' => '选择题', 'fill' => '填空题', 'answer' => '解答题'];
  777. $typeLabel = $typeMap[$q['question_type'] ?? ''] ?? ($q['question_type'] ?? '题型未标注');
  778. $questionText = is_string($q['question_text']) ? $q['question_text'] : json_encode($q['question_text'], JSON_UNESCAPED_UNICODE);
  779. $solution = $q['solution'] ?? null;
  780. if (is_string($solution)) {
  781. $solution = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solution);
  782. }
  783. $solution = $formatSolutionLikeGrading($solution);
  784. $analysis = $formatSolutionLikeGrading($analysis);
  785. // 对齐判卷渲染口径:直接走 MathFormulaProcessor,保留图片与公式标签
  786. $renderLikeGrading = function ($text) {
  787. if (is_array($text)) {
  788. $text = json_encode($text, JSON_UNESCAPED_UNICODE);
  789. }
  790. $text = is_string($text) ? trim($text) : '';
  791. if ($text === '') {
  792. return '';
  793. }
  794. return \App\Services\MathFormulaProcessor::processFormulas($text);
  795. };
  796. // 选择题:选项来自服务端 questions.options / 题干解析;正确答案字母用于打 ✅
  797. $displayCorrectAnswer = is_array($correctAnswer)
  798. ? json_encode($correctAnswer, JSON_UNESCAPED_UNICODE)
  799. : (string) $correctAnswer;
  800. $questionTypeRaw = strtolower(trim((string) ($q['question_type'] ?? '')));
  801. $isChoiceQuestion = in_array($questionTypeRaw, ['choice', 'multiple_choice', 'single_choice', '选择题', 'select'], true);
  802. $normalizedOptions = [];
  803. $correctAnswerLetters = [];
  804. if ($isChoiceQuestion) {
  805. $rawOptions = $q['options'] ?? [];
  806. if (is_string($rawOptions)) {
  807. $decodedOptions = json_decode($rawOptions, true);
  808. $rawOptions = is_array($decodedOptions) ? $decodedOptions : [];
  809. }
  810. if (is_array($rawOptions)) {
  811. foreach ($rawOptions as $optKey => $optValue) {
  812. $letter = null;
  813. if (is_string($optKey) && preg_match('/([A-H])/i', $optKey, $m)) {
  814. $letter = strtoupper($m[1]);
  815. } elseif (is_array($optValue)) {
  816. $candidate = $optValue['label'] ?? $optValue['key'] ?? $optValue['option'] ?? null;
  817. if (is_string($candidate) && preg_match('/([A-H])/i', $candidate, $m)) {
  818. $letter = strtoupper($m[1]);
  819. }
  820. }
  821. if ($letter === null) {
  822. continue;
  823. }
  824. $content = is_array($optValue)
  825. ? ($optValue['content'] ?? $optValue['text'] ?? $optValue['value'] ?? '')
  826. : $optValue;
  827. if (!is_string($content)) {
  828. $content = json_encode($content, JSON_UNESCAPED_UNICODE);
  829. }
  830. $content = trim((string) $content);
  831. if ($content !== '') {
  832. $normalizedOptions[$letter] = $content;
  833. }
  834. }
  835. }
  836. if (trim((string) $correctAnswer) !== '') {
  837. preg_match_all('/[A-H]/i', strtoupper((string) $correctAnswer), $answerMatches);
  838. $correctAnswerLetters = array_values(array_unique($answerMatches[0] ?? []));
  839. }
  840. if (!empty($normalizedOptions) && !empty($correctAnswerLetters)) {
  841. $mappedAnswers = [];
  842. foreach ($correctAnswerLetters as $letter) {
  843. if (isset($normalizedOptions[$letter])) {
  844. $mappedAnswers[] = $letter . '. ' . $normalizedOptions[$letter];
  845. }
  846. }
  847. if (!empty($mappedAnswers)) {
  848. $displayCorrectAnswer = implode(';', $mappedAnswers);
  849. }
  850. }
  851. }
  852. $choiceOptionLetters = !empty($normalizedOptions) ? array_keys($normalizedOptions) : [];
  853. sort($choiceOptionLetters);
  854. $choiceLayoutClass = 'options-grid-1';
  855. $layoutDecider = app(\App\Support\OptionLayoutDecider::class);
  856. if (! empty($normalizedOptions) && ! empty($choiceOptionLetters)) {
  857. $optValuesForLayout = [];
  858. foreach ($choiceOptionLetters as $L) {
  859. $optValuesForLayout[] = $normalizedOptions[$L];
  860. }
  861. $layoutMeta = $layoutDecider->decide($optValuesForLayout, 'grading');
  862. $choiceLayoutClass = $layoutMeta['class'] ?? 'options-grid-1';
  863. }
  864. @endphp
  865. <div class="question-card">
  866. <div style="display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:4px;">
  867. <div style="display:flex; align-items:center; gap:8px; font-weight:600;">
  868. <span class="tag">题号 {{ $q['display_number'] ?? $q['question_number'] }} · {{ $typeLabel }}</span>
  869. @php
  870. $kpName = $q['knowledge_point_name'] ?? $q['knowledge_point'] ?? null;
  871. if (!empty($kpName) && $kpName !== '-' && $kpName !== '未标注') {
  872. echo '<span class="tag" style="background: #eef2ff; color:#4338ca;">' . e($kpName) . '</span>';
  873. }
  874. @endphp
  875. @if($showStatus)
  876. <span class="tag" style="background: {{ $statusColor }}; color:#fff;">{{ $statusText }}</span>
  877. @endif
  878. </div>
  879. @if($score !== null && $fullScore !== null)
  880. <div class="muted">得分 {{ $score }} / {{ $fullScore }}</div>
  881. @endif
  882. </div>
  883. {{-- 题干:数据已在 ExamPdfExportService 中经 processQuestionData(含 <image>→<img>、公式);样式对齐判卷 question-stem --}}
  884. <div class="question-stem math-content" style="margin-bottom:6px;">{!! $questionText !!}</div>
  885. {{-- 选择题:与判卷页相同网格布局(OptionLayoutDecider),正确项旁 ✅ --}}
  886. @if(!empty($isChoiceQuestion) && !empty($normalizedOptions))
  887. <div class="report-options {{ $choiceLayoutClass }}">
  888. @foreach($choiceOptionLetters as $optLetter)
  889. @php
  890. $isCorrectOpt = in_array($optLetter, $correctAnswerLetters ?? [], true);
  891. $rawOpt = (string) ($normalizedOptions[$optLetter] ?? '');
  892. $normalizedOpt = str_replace('\\dfrac', '\\frac', $rawOpt);
  893. $normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
  894. $normalizedOpt = $layoutDecider->normalizeCompactMathForDisplay($normalizedOpt);
  895. $rawOptPlain = html_entity_decode(strip_tags($rawOpt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
  896. $rawOptPlain = preg_replace('/\s+/u', '', $rawOptPlain ?? '');
  897. $isShortOption = mb_strlen((string) $rawOptPlain, 'UTF-8') <= 8;
  898. $valClass = $isShortOption ? 'option-short' : 'option-long';
  899. $renderedOpt = $renderLikeGrading($normalizedOpt);
  900. @endphp
  901. <div class="option option-compact">
  902. <strong>{{ $optLetter }}.</strong>
  903. <span class="option-value {{ $valClass }}">{!! $renderedOpt !!}</span>
  904. @if($isCorrectOpt)
  905. <span style="margin-left:4px; font-size:13px; color:#15803d; font-weight:700;">✅</span>
  906. @endif
  907. </div>
  908. @endforeach
  909. </div>
  910. @endif
  911. {{-- 非选择题,或选择题未能得到选项列表时:显示「正确答案」文字框 --}}
  912. @if(!empty($correctAnswer) && (!$isChoiceQuestion || empty($normalizedOptions)))
  913. <div class="question-block" style="background:#f0fdf4; border-left:3px solid #10b981;">
  914. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">正确答案</div>
  915. <div class="math-content" style="line-height:1.7; color:#374151;">
  916. {!! $renderLikeGrading($displayCorrectAnswer) !!}
  917. </div>
  918. </div>
  919. @endif
  920. {{-- 解题思路:判卷页 answer-meta 口径(单行标题+正文,避免大块色条占行) --}}
  921. @if(!empty($solution))
  922. <div class="report-answer-meta">
  923. <div class="answer-line">
  924. <strong>解题思路:</strong>
  925. <span class="solution-content">{!! $renderLikeGrading($solution) !!}</span>
  926. </div>
  927. </div>
  928. @elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
  929. <div class="report-answer-meta">
  930. <div class="answer-line">
  931. <strong>解题思路:</strong>
  932. <span class="solution-content">{!! $renderLikeGrading($analysis) !!}</span>
  933. </div>
  934. </div>
  935. @endif
  936. {{-- 解题步骤(如果有) --}}
  937. @if(!empty($steps))
  938. <div style="margin-top:6px; font-size:12px;">
  939. <div style="font-weight:600; margin-bottom:3px;">解题步骤</div>
  940. <ol style="margin:0; padding-left:18px;">
  941. @foreach($steps as $s)
  942. <li style="margin-bottom:2px;">{!! nl2br(e(is_array($s) ? json_encode($s, JSON_UNESCAPED_UNICODE) : $s)) !!}</li>
  943. @endforeach
  944. </ol>
  945. </div>
  946. @endif
  947. </div>
  948. @endforeach
  949. </div>
  950. @endif
  951. </div> {{-- 闭合page div --}}
  952. <script src="/js/katex.min.js"></script>
  953. <script src="/js/auto-render.min.js"></script>
  954. <script>
  955. document.addEventListener('DOMContentLoaded', function() {
  956. try {
  957. renderMathInElement(document.body, {
  958. delimiters: [
  959. {left: "$$", right: "$$", display: true},
  960. {left: "$", right: "$", display: false},
  961. {left: "\\(", right: "\\)", display: false},
  962. {left: "\\[", right: "\\]", display: true}
  963. ],
  964. throwOnError: false,
  965. strict: false,
  966. trust: true
  967. });
  968. } catch (e) {}
  969. });
  970. </script>
  971. </body>
  972. </html>