question-preview.blade.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta name="csrf-token" content="{{ csrf_token() }}">
  7. <title>题目预览验证工具 - Math CMS</title>
  8. <!-- KaTeX CSS -->
  9. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
  10. <!-- Tailwind CSS CDN -->
  11. <script src="https://cdn.tailwindcss.com"></script>
  12. <!-- Alpine.js -->
  13. <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
  14. <!-- KaTeX JS -->
  15. <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
  16. <style>
  17. [x-cloak] { display: none !important; }
  18. .math-preview {
  19. font-family: "SimSun", "Songti SC", serif;
  20. line-height: 1.8;
  21. }
  22. .math-preview .katex {
  23. font-size: 1.1em;
  24. }
  25. .math-preview .katex-display {
  26. margin: 0.5em 0;
  27. }
  28. .question-stem {
  29. margin-bottom: 1rem;
  30. }
  31. .question-options {
  32. margin-left: 1rem;
  33. }
  34. .question-option {
  35. margin: 0.5rem 0;
  36. }
  37. .question-answer {
  38. margin-top: 1rem;
  39. padding-top: 0.5rem;
  40. border-top: 1px dashed #ccc;
  41. }
  42. .question-solution {
  43. margin-top: 1rem;
  44. padding: 0.75rem;
  45. background: #f9f9f9;
  46. border-radius: 4px;
  47. }
  48. </style>
  49. </head>
  50. <body class="bg-gray-100 min-h-screen">
  51. <div class="max-w-7xl mx-auto py-8 px-4" x-data="questionPreview()" x-cloak>
  52. <!-- 标题 -->
  53. <div class="text-center mb-8">
  54. <h1 class="text-2xl font-bold text-gray-800">题目预览验证工具</h1>
  55. <p class="text-gray-500 mt-2">验证题目在网页和PDF中的显示效果</p>
  56. </div>
  57. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
  58. <!-- 左侧:输入表单 -->
  59. <div class="bg-white rounded-lg shadow p-6">
  60. <h2 class="text-lg font-semibold text-gray-700 mb-4">输入题目内容</h2>
  61. <!-- 题干 -->
  62. <div class="mb-4">
  63. <label class="block text-sm font-medium text-gray-700 mb-1">
  64. 题干 <span class="text-red-500">*</span>
  65. </label>
  66. <textarea
  67. x-model="form.stem"
  68. rows="5"
  69. class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
  70. placeholder="粘贴题干内容,支持 LaTeX 公式(如 $$\sqrt{2}$$ 或 $x^2$)"
  71. ></textarea>
  72. </div>
  73. <!-- 选项 -->
  74. <div class="mb-4">
  75. <label class="block text-sm font-medium text-gray-700 mb-2">
  76. 选项(选填,不填则为填空/解答题)
  77. </label>
  78. <div class="grid grid-cols-2 gap-3">
  79. <div>
  80. <label class="text-xs text-gray-500">A</label>
  81. <input
  82. type="text"
  83. x-model="form.options.A"
  84. class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
  85. placeholder="选项 A"
  86. >
  87. </div>
  88. <div>
  89. <label class="text-xs text-gray-500">B</label>
  90. <input
  91. type="text"
  92. x-model="form.options.B"
  93. class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
  94. placeholder="选项 B"
  95. >
  96. </div>
  97. <div>
  98. <label class="text-xs text-gray-500">C</label>
  99. <input
  100. type="text"
  101. x-model="form.options.C"
  102. class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
  103. placeholder="选项 C"
  104. >
  105. </div>
  106. <div>
  107. <label class="text-xs text-gray-500">D</label>
  108. <input
  109. type="text"
  110. x-model="form.options.D"
  111. class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
  112. placeholder="选项 D"
  113. >
  114. </div>
  115. </div>
  116. </div>
  117. <!-- 答案 -->
  118. <div class="mb-4">
  119. <label class="block text-sm font-medium text-gray-700 mb-1">答案</label>
  120. <input
  121. type="text"
  122. x-model="form.answer"
  123. class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500"
  124. placeholder="正确答案"
  125. >
  126. </div>
  127. <!-- 解析 -->
  128. <div class="mb-6">
  129. <label class="block text-sm font-medium text-gray-700 mb-1">解析/解题思路</label>
  130. <textarea
  131. x-model="form.solution"
  132. rows="4"
  133. class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500"
  134. placeholder="解题思路或解析内容"
  135. ></textarea>
  136. </div>
  137. <!-- 操作按钮 -->
  138. <div class="flex flex-wrap gap-3">
  139. <button
  140. @click="previewWeb()"
  141. class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition flex items-center gap-2"
  142. >
  143. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  144. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
  145. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
  146. </svg>
  147. 网页预览
  148. </button>
  149. <button
  150. @click="previewPdf()"
  151. :disabled="pdfLoading"
  152. :class="pdfLoading ? 'opacity-50 cursor-wait' : ''"
  153. class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition flex items-center gap-2"
  154. >
  155. <svg x-show="!pdfLoading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  156. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
  157. </svg>
  158. <svg x-show="pdfLoading" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
  159. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  160. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  161. </svg>
  162. <span x-text="pdfLoading ? '生成中...' : 'PDF 预览'"></span>
  163. </button>
  164. <button
  165. @click="previewBoth()"
  166. :disabled="pdfLoading"
  167. class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition flex items-center gap-2"
  168. >
  169. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  170. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
  171. </svg>
  172. 同时预览
  173. </button>
  174. <button
  175. @click="clearForm()"
  176. class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition"
  177. >
  178. 清空
  179. </button>
  180. </div>
  181. </div>
  182. <!-- 右侧:预览区域 -->
  183. <div class="space-y-6">
  184. <!-- 网页预览 -->
  185. <div x-show="showWebPreview" class="bg-white rounded-lg shadow">
  186. <div class="px-4 py-3 border-b border-gray-200 flex items-center gap-2">
  187. <span class="text-blue-600">
  188. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  189. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
  190. </svg>
  191. </span>
  192. <h3 class="font-semibold text-gray-700">网页效果预览</h3>
  193. </div>
  194. <div id="web-preview-area" class="p-4 math-preview">
  195. <!-- 题干 -->
  196. <template x-if="form.stem">
  197. <div class="question-stem" x-html="renderMath(form.stem)"></div>
  198. </template>
  199. <!-- 选项 -->
  200. <template x-if="hasOptions()">
  201. <div class="question-options">
  202. <template x-if="form.options.A">
  203. <div class="question-option">
  204. <span class="font-medium">A.</span>
  205. <span x-html="renderMath(form.options.A)"></span>
  206. </div>
  207. </template>
  208. <template x-if="form.options.B">
  209. <div class="question-option">
  210. <span class="font-medium">B.</span>
  211. <span x-html="renderMath(form.options.B)"></span>
  212. </div>
  213. </template>
  214. <template x-if="form.options.C">
  215. <div class="question-option">
  216. <span class="font-medium">C.</span>
  217. <span x-html="renderMath(form.options.C)"></span>
  218. </div>
  219. </template>
  220. <template x-if="form.options.D">
  221. <div class="question-option">
  222. <span class="font-medium">D.</span>
  223. <span x-html="renderMath(form.options.D)"></span>
  224. </div>
  225. </template>
  226. </div>
  227. </template>
  228. <!-- 答案 -->
  229. <template x-if="form.answer">
  230. <div class="question-answer">
  231. <span class="font-medium text-green-700">答案:</span>
  232. <span x-html="renderMath(form.answer)"></span>
  233. </div>
  234. </template>
  235. <!-- 解析 -->
  236. <template x-if="form.solution">
  237. <div class="question-solution">
  238. <div class="font-medium text-gray-700 mb-2">解题思路:</div>
  239. <div x-html="renderMath(form.solution)"></div>
  240. </div>
  241. </template>
  242. </div>
  243. </div>
  244. <!-- PDF 预览 -->
  245. <div x-show="showPdfPreview || pdfError" class="bg-white rounded-lg shadow">
  246. <div class="px-4 py-3 border-b border-gray-200 flex items-center gap-2">
  247. <span class="text-green-600">
  248. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  249. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
  250. </svg>
  251. </span>
  252. <h3 class="font-semibold text-gray-700">PDF 效果预览</h3>
  253. <a x-show="pdfUrl" :href="pdfUrl" target="_blank" class="ml-auto text-sm text-blue-600 hover:underline">
  254. 新窗口打开
  255. </a>
  256. </div>
  257. <div class="p-4">
  258. <template x-if="pdfError">
  259. <div class="text-red-600 bg-red-50 p-4 rounded">
  260. <p class="font-medium">生成失败</p>
  261. <p class="text-sm mt-1" x-text="pdfError"></p>
  262. </div>
  263. </template>
  264. <template x-if="pdfUrl && !pdfError">
  265. <iframe
  266. :src="pdfUrl"
  267. class="w-full h-[600px] border border-gray-200 rounded"
  268. ></iframe>
  269. </template>
  270. </div>
  271. </div>
  272. <!-- 使用提示 -->
  273. <div x-show="!showWebPreview && !showPdfPreview && !pdfError" class="bg-gray-50 rounded-lg p-6 text-center text-gray-500">
  274. <svg class="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  275. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
  276. </svg>
  277. <p>在左侧输入题目内容</p>
  278. <p class="text-sm mt-1">点击预览按钮查看渲染效果</p>
  279. <div class="mt-4 text-left text-xs text-gray-400 bg-white p-3 rounded border">
  280. <p class="font-medium mb-2">支持的 LaTeX 格式:</p>
  281. <ul class="space-y-1">
  282. <li><code class="bg-gray-100 px-1">$...$</code> 行内公式</li>
  283. <li><code class="bg-gray-100 px-1">$$...$$</code> 块级公式</li>
  284. <li><code class="bg-gray-100 px-1">\(...\)</code> 行内公式(会自动转换)</li>
  285. <li><code class="bg-gray-100 px-1">\[...\]</code> 块级公式(会自动转换)</li>
  286. </ul>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. </div>
  292. <script>
  293. /**
  294. * 数学公式渲染器
  295. */
  296. const MathRenderer = {
  297. preprocessText(text) {
  298. let result = text;
  299. // 1. 处理公式内的双反斜杠 -> 单反斜杠
  300. result = result.replace(/\$\$([\s\S]*?)\$\$/g, (_, tex) => {
  301. return '$$' + tex.replace(/\\\\/g, '\\') + '$$';
  302. });
  303. result = result.replace(/\$([^$\n]+?)\$/g, (_, tex) => {
  304. return '$' + tex.replace(/\\\\/g, '\\') + '$';
  305. });
  306. // 2. 换行符处理:\n -> <br>,但保护 LaTeX 命令如 \neq, \nu
  307. result = result.replace(/\\n(?![a-zA-Z])/g, '<br>');
  308. // 3. 统一 LaTeX 分隔符格式
  309. result = result.replace(/\\\[([\\s\S]*?)\\\]/g, (_, tex) => `$$${tex}$$`);
  310. result = result.replace(/\\\(([\\s\S]*?)\\\)/g, (_, tex) => `$${tex}$`);
  311. return result;
  312. },
  313. render(text) {
  314. if (!text) return '';
  315. let result = this.preprocessText(text);
  316. // 渲染块级公式 $$...$$
  317. result = result.replace(/\$\$([\s\S]*?)\$\$/g, (_, tex) => {
  318. try {
  319. return katex.renderToString(tex.trim(), {
  320. displayMode: true,
  321. throwOnError: false,
  322. strict: false,
  323. });
  324. } catch (e) {
  325. console.error('KaTeX render error:', e);
  326. return `<span class="text-red-500">$$${tex}$$</span>`;
  327. }
  328. });
  329. // 渲染行内公式 $...$
  330. result = result.replace(/\$([^$\n]+?)\$/g, (_, tex) => {
  331. try {
  332. return katex.renderToString(tex.trim(), {
  333. displayMode: false,
  334. throwOnError: false,
  335. strict: false,
  336. });
  337. } catch (e) {
  338. console.error('KaTeX render error:', e);
  339. return `<span class="text-red-500">$${tex}$</span>`;
  340. }
  341. });
  342. return result;
  343. }
  344. };
  345. function questionPreview() {
  346. return {
  347. form: {
  348. stem: '',
  349. options: { A: '', B: '', C: '', D: '' },
  350. answer: '',
  351. solution: ''
  352. },
  353. showWebPreview: false,
  354. showPdfPreview: false,
  355. pdfUrl: '',
  356. pdfError: '',
  357. pdfLoading: false,
  358. hasOptions() {
  359. return this.form.options.A || this.form.options.B || this.form.options.C || this.form.options.D;
  360. },
  361. renderMath(text) {
  362. return MathRenderer.render(text);
  363. },
  364. previewWeb() {
  365. if (!this.form.stem) {
  366. alert('请输入题干内容');
  367. return;
  368. }
  369. this.showWebPreview = true;
  370. },
  371. async previewPdf() {
  372. if (!this.form.stem) {
  373. alert('请输入题干内容');
  374. return;
  375. }
  376. this.pdfLoading = true;
  377. this.pdfError = '';
  378. this.pdfUrl = '';
  379. try {
  380. const response = await fetch('{{ route("tools.question-preview.pdf") }}', {
  381. method: 'POST',
  382. headers: {
  383. 'Content-Type': 'application/json',
  384. 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
  385. 'Accept': 'application/json'
  386. },
  387. body: JSON.stringify(this.form)
  388. });
  389. const data = await response.json();
  390. if (data.success) {
  391. this.pdfUrl = data.url;
  392. this.showPdfPreview = true;
  393. } else {
  394. this.pdfError = data.error || '生成失败';
  395. this.showPdfPreview = true;
  396. }
  397. } catch (e) {
  398. this.pdfError = e.message || '网络错误';
  399. this.showPdfPreview = true;
  400. } finally {
  401. this.pdfLoading = false;
  402. }
  403. },
  404. previewBoth() {
  405. this.previewWeb();
  406. this.previewPdf();
  407. },
  408. clearForm() {
  409. this.form = {
  410. stem: '',
  411. options: { A: '', B: '', C: '', D: '' },
  412. answer: '',
  413. solution: ''
  414. };
  415. this.showWebPreview = false;
  416. this.showPdfPreview = false;
  417. this.pdfUrl = '';
  418. this.pdfError = '';
  419. }
  420. };
  421. }
  422. </script>
  423. </body>
  424. </html>