edit.html 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370
  1. {% extends "layout.html" %}
  2. {% block page_title %}编辑题目 - {{ q.question_code }}{% endblock %}
  3. {% block content %}
  4. <div class="mb-8 no-print">
  5. <a href="/detail/{{ q.question_code }}" class="text-blue-600 font-medium hover:underline">← 取消编辑</a>
  6. </div>
  7. <!-- LaTeX 实时预览气泡 -->
  8. <div id="latex-preview-bubble" class="fixed right-8 top-24 w-[420px] max-h-[75vh] bg-white rounded-2xl shadow-2xl border border-gray-200 z-50 hidden overflow-hidden flex flex-col" style="box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
  9. <div class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-3 flex items-center justify-between">
  10. <span class="text-sm font-bold">实时预览</span>
  11. <button type="button" onclick="hidePreviewBubble()" class="text-white hover:text-gray-200 text-xl leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/20 transition-colors">×</button>
  12. </div>
  13. <div id="latex-preview-content" class="flex-1 p-6 overflow-y-auto bg-gray-50 text-base leading-relaxed" style="min-height: 200px;">
  14. <p class="text-sm text-gray-400 text-center">聚焦输入框查看预览</p>
  15. </div>
  16. </div>
  17. <form id="edit-form" class="grid grid-cols-1 gap-4">
  18. <input type="hidden" name="question_code" value="{{ q.question_code }}">
  19. <div class="apple-card p-6 space-y-4">
  20. <div class="border-b border-gray-100 pb-3 mb-3">
  21. <div class="flex items-center gap-3 flex-wrap">
  22. <h2 class="text-xl font-bold">编辑题目</h2>
  23. </div>
  24. <p class="text-gray-400 text-xs mt-1">修改题目信息,保存后生效</p>
  25. </div>
  26. <!-- 题型和难度选择 -->
  27. <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
  28. <div class="space-y-1">
  29. <label class="text-xs font-bold text-gray-400 uppercase">题型</label>
  30. <select name="question_type" id="question-type-select" data-original="{{ q.question_type or '' }}" class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
  31. <option value="">请选择题型</option>
  32. <option value="choice" {% if q.question_type == 'choice' %}selected{% endif %}>选择题</option>
  33. <option value="fill" {% if q.question_type == 'fill' %}selected{% endif %}>填空题</option>
  34. <option value="answer" {% if q.question_type == 'answer' %}selected{% endif %}>解答题</option>
  35. </select>
  36. </div>
  37. <!-- 难度选择 -->
  38. <div id="difficulty-section" class="space-y-1">
  39. <label class="text-xs font-bold text-gray-400 uppercase block">难度</label>
  40. <div class="flex items-center gap-2">
  41. <select name="difficulty" id="difficulty-select" data-original="{{ q.difficulty or '' }}" class="flex-1 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm">
  42. <option value="">请选择难度</option>
  43. {% set diff = q.difficulty %}
  44. {% if diff %}
  45. {% set diff_str = diff | string %}
  46. {% set diff_float = diff | float if diff is number else 0.0 %}
  47. {% else %}
  48. {% set diff_str = '' %}
  49. {% set diff_float = 0.0 %}
  50. {% endif %}
  51. <option value="0.2" {% if diff_str == '0.2' or diff == 0.2 or (diff_float >= 0.19 and diff_float <= 0.21) %}selected{% endif %}>筑基</option>
  52. <option value="0.4" {% if diff_str == '0.4' or diff == 0.4 or (diff_float >= 0.39 and diff_float <= 0.41) %}selected{% endif %}>提分</option>
  53. <option value="0.7" {% if diff_str == '0.7' or diff == 0.7 or (diff_float >= 0.69 and diff_float <= 0.71) %}selected{% endif %}>培优</option>
  54. </select>
  55. <button type="button" id="evaluate-difficulty-btn" onclick="evaluateDifficulty()" class="btn-apple bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 text-xs py-1 px-2 shadow-md whitespace-nowrap flex-shrink-0">
  56. <svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  57. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
  58. </svg>
  59. 难度评价
  60. </button>
  61. </div>
  62. </div>
  63. </div>
  64. <!-- 题干编辑 -->
  65. <div class="space-y-2">
  66. <div class="flex items-center justify-between">
  67. <label class="text-xs font-bold text-gray-400 uppercase">题干 <span class="text-red-500">*</span> (支持 LaTeX 和 HTML/SVG)</label>
  68. <button
  69. type="button"
  70. id="upload-image-btn"
  71. class="btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-3 h-fit"
  72. onclick="triggerStemImageUpload()"
  73. >
  74. 上传图片
  75. </button>
  76. </div>
  77. <textarea
  78. id="stem-textarea"
  79. name="stem"
  80. data-original="{{ q.stem | tojson }}"
  81. required
  82. class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm"
  83. placeholder="请输入题干内容或拖拽图片..."
  84. ondrop="handleStemDrop(event)"
  85. ondragover="handleStemDragOver(event)"
  86. ondragleave="handleStemDragLeave(event)"
  87. >{{ q.stem or '' }}</textarea>
  88. <div id="upload-status" class="mt-1 text-xs hidden"></div>
  89. </div>
  90. <!-- 选项编辑 -->
  91. <div id="options-section" class="space-y-2">
  92. <label class="text-xs font-bold text-gray-400 uppercase">选项</label>
  93. <!-- 选项输入区域 - 2列布局 -->
  94. <div class="grid grid-cols-2 gap-3" id="options-container">
  95. <!-- 选项A -->
  96. <div class="option-item" data-option="A">
  97. <label class="text-xs font-medium text-gray-600 mb-1 block">选项 A</label>
  98. <div class="flex gap-1.5">
  99. <textarea
  100. name="option_A"
  101. id="option-A-input"
  102. class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
  103. placeholder="输入选项A的内容或拖拽图片..."
  104. oninput="updateOptionsPreview()"
  105. ondrop="handleOptionDrop(event, 'A')"
  106. ondragover="handleOptionDragOver(event)"
  107. ondragleave="handleOptionDragLeave(event)"
  108. ></textarea>
  109. <button
  110. type="button"
  111. class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
  112. onclick="uploadOptionImage('A')"
  113. >
  114. 上传图片
  115. </button>
  116. </div>
  117. </div>
  118. <!-- 选项B -->
  119. <div class="option-item" data-option="B">
  120. <label class="text-xs font-medium text-gray-600 mb-1 block">选项 B</label>
  121. <div class="flex gap-1.5">
  122. <textarea
  123. name="option_B"
  124. id="option-B-input"
  125. class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
  126. placeholder="输入选项B的内容或拖拽图片..."
  127. oninput="updateOptionsPreview()"
  128. ondrop="handleOptionDrop(event, 'B')"
  129. ondragover="handleOptionDragOver(event)"
  130. ondragleave="handleOptionDragLeave(event)"
  131. ></textarea>
  132. <button
  133. type="button"
  134. class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
  135. onclick="uploadOptionImage('B')"
  136. >
  137. 上传图片
  138. </button>
  139. </div>
  140. </div>
  141. <!-- 选项C -->
  142. <div class="option-item" data-option="C">
  143. <label class="text-xs font-medium text-gray-600 mb-1 block">选项 C</label>
  144. <div class="flex gap-1.5">
  145. <textarea
  146. name="option_C"
  147. id="option-C-input"
  148. class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
  149. placeholder="输入选项C的内容或拖拽图片..."
  150. oninput="updateOptionsPreview()"
  151. ondrop="handleOptionDrop(event, 'C')"
  152. ondragover="handleOptionDragOver(event)"
  153. ondragleave="handleOptionDragLeave(event)"
  154. ></textarea>
  155. <button
  156. type="button"
  157. class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
  158. onclick="uploadOptionImage('C')"
  159. >
  160. 上传图片
  161. </button>
  162. </div>
  163. </div>
  164. <!-- 选项D -->
  165. <div class="option-item" data-option="D">
  166. <label class="text-xs font-medium text-gray-600 mb-1 block">选项 D</label>
  167. <div class="flex gap-1.5">
  168. <textarea
  169. name="option_D"
  170. id="option-D-input"
  171. class="flex-1 h-12 p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/10 outline-none transition-all text-xs"
  172. placeholder="输入选项D的内容或拖拽图片..."
  173. oninput="updateOptionsPreview()"
  174. ondrop="handleOptionDrop(event, 'D')"
  175. ondragover="handleOptionDragOver(event)"
  176. ondragleave="handleOptionDragLeave(event)"
  177. ></textarea>
  178. <button
  179. type="button"
  180. class="option-upload-btn btn-apple bg-blue-600 text-white hover:bg-blue-700 text-xs py-1.5 px-2 h-fit self-start"
  181. onclick="uploadOptionImage('D')"
  182. >
  183. 上传图片
  184. </button>
  185. </div>
  186. </div>
  187. </div>
  188. <!-- 选项预览(可编辑) -->
  189. <div class="mt-2 p-2 rounded-lg bg-gray-50 border border-gray-200">
  190. <label class="text-xs font-bold text-gray-600 mb-1 block">选项预览 (JSON格式,可直接编辑)</label>
  191. <textarea
  192. id="options-preview"
  193. data-original="{{ q.options | tojson }}"
  194. class="w-full text-xs text-gray-700 font-mono bg-white p-2 rounded border border-gray-100 min-h-[80px] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all resize-y"
  195. placeholder='{"A": "选项A内容", "B": "选项B内容", ...}'
  196. oninput="syncOptionsFromPreview()"
  197. >{{ q.options or '{}' }}</textarea>
  198. <input type="hidden" name="options" id="options-json-input">
  199. </div>
  200. </div>
  201. <!-- 答案 -->
  202. <div class="space-y-1">
  203. <label class="text-xs font-bold text-gray-400 uppercase">正确答案 <span class="text-red-500">*</span></label>
  204. <input type="text" name="answer" id="answer-input" data-original="{{ q.answer | tojson }}" value="{{ q.answer or '' }}" required class="w-full p-2 rounded-lg bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all text-sm" placeholder="例如: A">
  205. </div>
  206. <!-- 解析 -->
  207. <div class="space-y-2">
  208. <label class="text-xs font-bold text-gray-400 uppercase">解析 <span class="text-red-500">*</span></label>
  209. <textarea
  210. id="solution-textarea"
  211. name="solution"
  212. data-original="{{ q.solution | tojson }}"
  213. required
  214. class="w-full h-32 p-3 rounded-xl bg-gray-50 border border-gray-100 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10 outline-none transition-all font-mono text-sm"
  215. placeholder="请输入解析内容或拖拽图片..."
  216. ondrop="handleSolutionDrop(event)"
  217. ondragover="handleSolutionDragOver(event)"
  218. ondragleave="handleSolutionDragLeave(event)"
  219. >{{ q.solution or '' }}</textarea>
  220. </div>
  221. <!-- 保存按钮 -->
  222. <div class="pt-4 flex justify-end space-x-2">
  223. <button type="button" onclick="window.location.href='/detail/{{ q.question_code }}'" class="btn-apple bg-gray-100 text-gray-700 hover:bg-gray-200 text-sm py-2 px-4">取消</button>
  224. <button type="submit" class="btn-apple bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200 text-sm py-2 px-4">保存修改</button>
  225. </div>
  226. </div>
  227. </form>
  228. <script>
  229. // 工具函数:保留两位小数
  230. function round(value, decimals) {
  231. return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
  232. }
  233. // LaTeX 实时预览功能
  234. let previewUpdateTimer = null;
  235. let currentPreviewElement = null;
  236. function showPreviewBubble(element, label) {
  237. const bubble = document.getElementById('latex-preview-bubble');
  238. const content = document.getElementById('latex-preview-content');
  239. if (!bubble || !content) return;
  240. // 更新标题
  241. const title = bubble.querySelector('.bg-gradient-to-r span');
  242. if (title) {
  243. title.textContent = label || '实时预览';
  244. }
  245. currentPreviewElement = element;
  246. bubble.classList.remove('hidden');
  247. updatePreviewContent(element.value || '');
  248. }
  249. function hidePreviewBubble() {
  250. const bubble = document.getElementById('latex-preview-bubble');
  251. if (bubble) {
  252. bubble.classList.add('hidden');
  253. }
  254. currentPreviewElement = null;
  255. }
  256. function updatePreviewContent(text) {
  257. const content = document.getElementById('latex-preview-content');
  258. if (!content) return;
  259. if (!text || !text.trim()) {
  260. content.innerHTML = '<p class="text-sm text-gray-400 text-center">聚焦输入框查看预览</p>';
  261. return;
  262. }
  263. // 将文本内容转换为 HTML,保留换行和基本格式
  264. let html = text
  265. .replace(/&/g, '&amp;')
  266. .replace(/</g, '&lt;')
  267. .replace(/>/g, '&gt;')
  268. .replace(/\n/g, '<br>');
  269. // 处理图片标签
  270. html = html.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
  271. content.innerHTML = html;
  272. // 等待 KaTeX 加载完成后渲染数学公式
  273. if (window.renderMathInElement) {
  274. try {
  275. window.renderMathInElement(content, {
  276. delimiters: [
  277. {left: "$$", right: "$$", display: true},
  278. {left: "$", right: "$", display: false},
  279. {left: "\\(", right: "\\)", display: false},
  280. {left: "\\[", right: "\\]", display: true}
  281. ],
  282. throwOnError: false
  283. });
  284. } catch (e) {
  285. console.warn('LaTeX 渲染失败:', e);
  286. }
  287. } else {
  288. // 如果 KaTeX 还没加载,等待一下再试
  289. setTimeout(() => {
  290. if (window.renderMathInElement) {
  291. try {
  292. window.renderMathInElement(content, {
  293. delimiters: [
  294. {left: "$$", right: "$$", display: true},
  295. {left: "$", right: "$", display: false},
  296. {left: "\\(", right: "\\)", display: false},
  297. {left: "\\[", right: "\\]", display: true}
  298. ],
  299. throwOnError: false
  300. });
  301. } catch (e) {
  302. console.warn('LaTeX 渲染失败:', e);
  303. }
  304. }
  305. }, 100);
  306. }
  307. }
  308. function setupPreviewForElement(elementId, label) {
  309. const element = document.getElementById(elementId);
  310. if (!element) return;
  311. element.addEventListener('focus', function() {
  312. showPreviewBubble(this, label);
  313. updatePreviewContent(this.value || '');
  314. });
  315. element.addEventListener('input', function() {
  316. if (currentPreviewElement === this) {
  317. // 防抖处理,避免频繁更新
  318. clearTimeout(previewUpdateTimer);
  319. previewUpdateTimer = setTimeout(() => {
  320. updatePreviewContent(this.value || '');
  321. }, 300);
  322. }
  323. });
  324. element.addEventListener('blur', function() {
  325. // 不自动隐藏,保持预览气泡显示
  326. });
  327. }
  328. // 更新选项预览气泡内容(显示4个选项的渲染)
  329. function updateOptionsPreviewBubble() {
  330. const previewTextarea = document.getElementById('options-preview');
  331. const bubble = document.getElementById('latex-preview-bubble');
  332. const content = document.getElementById('latex-preview-content');
  333. if (!previewTextarea || !bubble || !content) return;
  334. const jsonStr = previewTextarea.value.trim();
  335. if (!jsonStr || jsonStr === '{}') {
  336. content.innerHTML = '<p class="text-sm text-gray-400 text-center">暂无选项内容</p>';
  337. return;
  338. }
  339. try {
  340. const optionsObj = JSON.parse(jsonStr);
  341. const optionKeys = ['A', 'B', 'C', 'D'];
  342. let html = '<div class="space-y-4">';
  343. optionKeys.forEach(key => {
  344. const optionText = optionsObj[key] || '';
  345. html += `<div class="border-b border-gray-200 pb-3 last:border-0 last:pb-0">`;
  346. html += `<div class="text-xs font-bold text-gray-500 mb-2">选项 ${key}</div>`;
  347. if (!optionText || !optionText.trim()) {
  348. html += `<p class="text-xs text-gray-400">暂无内容</p>`;
  349. } else {
  350. // 将文本内容转换为 HTML,保留换行和基本格式
  351. let optionHtml = optionText
  352. .replace(/&/g, '&amp;')
  353. .replace(/</g, '&lt;')
  354. .replace(/>/g, '&gt;')
  355. .replace(/\n/g, '<br>');
  356. // 处理图片标签
  357. optionHtml = optionHtml.replace(/&lt;image\s+src="([^"]+)"\s*\/?&gt;/gi, '<img src="$1" class="max-w-[400px] max-h-[300px] w-auto h-auto my-2 rounded-lg mx-auto block" alt="题目图片" style="object-fit: contain;">');
  358. html += `<div class="text-sm leading-relaxed">${optionHtml}</div>`;
  359. }
  360. html += `</div>`;
  361. });
  362. html += '</div>';
  363. content.innerHTML = html;
  364. // 等待 KaTeX 加载完成后渲染数学公式
  365. if (window.renderMathInElement) {
  366. try {
  367. window.renderMathInElement(content, {
  368. delimiters: [
  369. {left: "$$", right: "$$", display: true},
  370. {left: "$", right: "$", display: false},
  371. {left: "\\(", right: "\\)", display: false},
  372. {left: "\\[", right: "\\]", display: true}
  373. ],
  374. throwOnError: false
  375. });
  376. } catch (e) {
  377. console.warn('LaTeX 渲染失败:', e);
  378. }
  379. } else {
  380. setTimeout(() => {
  381. if (window.renderMathInElement) {
  382. try {
  383. window.renderMathInElement(content, {
  384. delimiters: [
  385. {left: "$$", right: "$$", display: true},
  386. {left: "$", right: "$", display: false},
  387. {left: "\\(", right: "\\)", display: false},
  388. {left: "\\[", right: "\\]", display: true}
  389. ],
  390. throwOnError: false
  391. });
  392. } catch (e) {
  393. console.warn('LaTeX 渲染失败:', e);
  394. }
  395. }
  396. }, 100);
  397. }
  398. } catch (error) {
  399. content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
  400. }
  401. }
  402. // 设置选项预览输入框的预览功能
  403. function setupOptionsPreviewTextarea() {
  404. const optionsPreviewTextarea = document.getElementById('options-preview');
  405. if (!optionsPreviewTextarea) return;
  406. optionsPreviewTextarea.addEventListener('focus', function() {
  407. showPreviewBubble(this, '选项预览');
  408. updateOptionsPreviewBubble();
  409. });
  410. optionsPreviewTextarea.addEventListener('input', function() {
  411. if (currentPreviewElement === this) {
  412. clearTimeout(previewUpdateTimer);
  413. previewUpdateTimer = setTimeout(() => {
  414. updateOptionsPreviewBubble();
  415. }, 300);
  416. }
  417. });
  418. optionsPreviewTextarea.addEventListener('blur', function() {
  419. // 不自动隐藏,保持预览气泡显示
  420. });
  421. }
  422. // 根据题型显示/隐藏选项相关内容
  423. function toggleOptionsVisibility(questionType) {
  424. const optionsSection = document.getElementById('options-section');
  425. const difficultySection = document.getElementById('difficulty-section');
  426. if (optionsSection) {
  427. if (questionType === 'choice') {
  428. optionsSection.style.display = 'block';
  429. } else {
  430. optionsSection.style.display = 'none';
  431. }
  432. }
  433. // 难度选择框:所有题型都显示
  434. if (difficultySection) {
  435. difficultySection.style.display = 'block';
  436. }
  437. }
  438. // 更新选项预览(从输入框同步到预览区域)
  439. function updateOptionsPreview() {
  440. const optionsObj = {};
  441. const optionKeys = ['A', 'B', 'C', 'D', 'E', 'F'];
  442. optionKeys.forEach(key => {
  443. const input = document.getElementById(`option-${key}-input`);
  444. if (input && input.value && input.value.trim()) {
  445. optionsObj[key] = input.value.trim();
  446. }
  447. });
  448. const preview = document.getElementById('options-preview');
  449. const jsonInput = document.getElementById('options-json-input');
  450. if (Object.keys(optionsObj).length > 0) {
  451. const jsonStr = JSON.stringify(optionsObj, null, 2);
  452. preview.value = jsonStr;
  453. jsonInput.value = JSON.stringify(optionsObj);
  454. } else {
  455. preview.value = '{}';
  456. jsonInput.value = '';
  457. }
  458. }
  459. // 从预览区域同步到选项输入框(当用户直接编辑预览时)
  460. function syncOptionsFromPreview() {
  461. const preview = document.getElementById('options-preview');
  462. const jsonInput = document.getElementById('options-json-input');
  463. try {
  464. const jsonStr = preview.value.trim();
  465. if (!jsonStr || jsonStr === '{}') {
  466. ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
  467. const input = document.getElementById(`option-${key}-input`);
  468. if (input) {
  469. input.value = '';
  470. }
  471. });
  472. jsonInput.value = '';
  473. if (currentPreviewElement === preview) {
  474. updateOptionsPreviewBubble();
  475. }
  476. return;
  477. }
  478. const optionsObj = JSON.parse(jsonStr);
  479. jsonInput.value = JSON.stringify(optionsObj);
  480. ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
  481. const input = document.getElementById(`option-${key}-input`);
  482. if (input) {
  483. input.value = optionsObj[key] || '';
  484. }
  485. });
  486. if (currentPreviewElement === preview) {
  487. updateOptionsPreviewBubble();
  488. }
  489. } catch (error) {
  490. console.warn('选项预览 JSON 格式错误:', error);
  491. if (currentPreviewElement === preview) {
  492. const content = document.getElementById('latex-preview-content');
  493. if (content) {
  494. content.innerHTML = '<p class="text-sm text-red-400 text-center">JSON 格式错误,请检查输入</p>';
  495. }
  496. }
  497. }
  498. }
  499. // 选项输入框拖拽处理
  500. function handleOptionDragOver(e) {
  501. e.preventDefault();
  502. e.stopPropagation();
  503. e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
  504. }
  505. function handleOptionDragLeave(e) {
  506. e.preventDefault();
  507. e.stopPropagation();
  508. e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
  509. }
  510. async function handleOptionDrop(e, optionKey) {
  511. e.preventDefault();
  512. e.stopPropagation();
  513. const textarea = e.currentTarget;
  514. textarea.classList.remove('border-blue-500', 'bg-blue-50');
  515. const files = e.dataTransfer.files;
  516. if (files && files.length > 0) {
  517. const file = files[0];
  518. if (file.type.startsWith('image/')) {
  519. await uploadOptionImageFile(file, optionKey);
  520. } else {
  521. if (window.customAlert) {
  522. window.customAlert('请拖拽图片文件!');
  523. } else {
  524. alert('请拖拽图片文件!');
  525. }
  526. }
  527. }
  528. }
  529. // 上传选项图片(通过文件选择)
  530. async function uploadOptionImage(optionKey) {
  531. const fileInput = document.createElement('input');
  532. fileInput.type = 'file';
  533. fileInput.accept = 'image/*';
  534. fileInput.style.display = 'none';
  535. fileInput.onchange = async (e) => {
  536. const file = e.target.files[0];
  537. if (!file) return;
  538. if (!file.type.startsWith('image/')) {
  539. if (window.customAlert) {
  540. window.customAlert('请选择图片文件!');
  541. } else {
  542. alert('请选择图片文件!');
  543. }
  544. return;
  545. }
  546. await uploadOptionImageFile(file, optionKey);
  547. };
  548. document.body.appendChild(fileInput);
  549. fileInput.click();
  550. }
  551. // 通用的选项图片上传函数
  552. async function uploadOptionImageFile(file, optionKey) {
  553. const optionInput = document.getElementById(`option-${optionKey}-input`);
  554. const uploadBtn = document.querySelector(`[onclick="uploadOptionImage('${optionKey}')"]`);
  555. let originalText = '';
  556. if (uploadBtn) {
  557. originalText = uploadBtn.textContent;
  558. uploadBtn.disabled = true;
  559. uploadBtn.textContent = '上传中...';
  560. }
  561. const originalPlaceholder = optionInput.placeholder;
  562. optionInput.placeholder = '正在上传图片...';
  563. optionInput.style.opacity = '0.6';
  564. try {
  565. const formData = new FormData();
  566. formData.append('file', file);
  567. const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
  568. method: 'POST',
  569. body: formData
  570. });
  571. if (!response.ok) {
  572. throw new Error(`上传失败: ${response.status} ${response.statusText}`);
  573. }
  574. const result = await response.json();
  575. let imageUrl = null;
  576. if (typeof result === 'string') {
  577. imageUrl = result;
  578. } else if (result.url) {
  579. imageUrl = result.url;
  580. } else if (result.data && result.data.url) {
  581. imageUrl = result.data.url;
  582. } else if (result.data && typeof result.data === 'string') {
  583. imageUrl = result.data;
  584. } else {
  585. const resultStr = JSON.stringify(result);
  586. const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
  587. if (urlMatch) {
  588. imageUrl = urlMatch[0];
  589. }
  590. }
  591. if (!imageUrl) {
  592. throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
  593. }
  594. const imageTag = `<image src="${imageUrl}"/>`;
  595. const cursorPos = optionInput.selectionStart;
  596. const textBefore = optionInput.value.substring(0, cursorPos);
  597. const textAfter = optionInput.value.substring(cursorPos);
  598. optionInput.value = textBefore + imageTag + textAfter;
  599. const newCursorPos = cursorPos + imageTag.length;
  600. optionInput.setSelectionRange(newCursorPos, newCursorPos);
  601. optionInput.focus();
  602. updateOptionsPreview();
  603. const optionsPreviewTextarea = document.getElementById('options-preview');
  604. if (currentPreviewElement === optionsPreviewTextarea) {
  605. updateOptionsPreviewBubble();
  606. }
  607. } catch (error) {
  608. console.error('上传失败:', error);
  609. if (window.customAlert) {
  610. window.customAlert('图片上传失败: ' + error.message);
  611. } else {
  612. alert('图片上传失败: ' + error.message);
  613. }
  614. } finally {
  615. if (uploadBtn) {
  616. uploadBtn.disabled = false;
  617. uploadBtn.textContent = originalText;
  618. }
  619. optionInput.placeholder = originalPlaceholder;
  620. optionInput.style.opacity = '1';
  621. }
  622. }
  623. // 触发题干图片上传
  624. function triggerStemImageUpload() {
  625. const fileInput = document.createElement('input');
  626. fileInput.type = 'file';
  627. fileInput.accept = 'image/*';
  628. fileInput.style.display = 'none';
  629. fileInput.onchange = async (e) => {
  630. const file = e.target.files[0];
  631. if (file) {
  632. await uploadStemImageFile(file);
  633. }
  634. };
  635. document.body.appendChild(fileInput);
  636. fileInput.click();
  637. document.body.removeChild(fileInput);
  638. }
  639. // 题干拖拽处理函数
  640. function handleStemDragOver(e) {
  641. e.preventDefault();
  642. e.stopPropagation();
  643. e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
  644. }
  645. function handleStemDragLeave(e) {
  646. e.preventDefault();
  647. e.stopPropagation();
  648. e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
  649. }
  650. async function handleStemDrop(e) {
  651. e.preventDefault();
  652. e.stopPropagation();
  653. const textarea = e.currentTarget;
  654. textarea.classList.remove('border-blue-500', 'bg-blue-50');
  655. const files = e.dataTransfer.files;
  656. if (files && files.length > 0) {
  657. const file = files[0];
  658. if (file.type.startsWith('image/')) {
  659. await uploadStemImageFile(file);
  660. } else {
  661. if (window.customAlert) {
  662. window.customAlert('请拖拽图片文件!');
  663. } else {
  664. alert('请拖拽图片文件!');
  665. }
  666. }
  667. }
  668. }
  669. // 解析拖拽处理函数
  670. function handleSolutionDragOver(e) {
  671. e.preventDefault();
  672. e.stopPropagation();
  673. e.currentTarget.classList.add('border-blue-500', 'bg-blue-50');
  674. }
  675. function handleSolutionDragLeave(e) {
  676. e.preventDefault();
  677. e.stopPropagation();
  678. e.currentTarget.classList.remove('border-blue-500', 'bg-blue-50');
  679. }
  680. async function handleSolutionDrop(e) {
  681. e.preventDefault();
  682. e.stopPropagation();
  683. const textarea = e.currentTarget;
  684. textarea.classList.remove('border-blue-500', 'bg-blue-50');
  685. const files = e.dataTransfer.files;
  686. if (files && files.length > 0) {
  687. const file = files[0];
  688. if (file.type.startsWith('image/')) {
  689. await uploadSolutionImageFile(file);
  690. } else {
  691. if (window.customAlert) {
  692. window.customAlert('请拖拽图片文件!');
  693. } else {
  694. alert('请拖拽图片文件!');
  695. }
  696. }
  697. }
  698. }
  699. // 题干图片上传函数
  700. async function uploadStemImageFile(file) {
  701. const stemTextarea = document.getElementById('stem-textarea');
  702. const statusDiv = document.getElementById('upload-status');
  703. const originalPlaceholder = stemTextarea.placeholder;
  704. stemTextarea.placeholder = '正在上传图片...';
  705. stemTextarea.style.opacity = '0.6';
  706. if (statusDiv) {
  707. statusDiv.classList.remove('hidden');
  708. statusDiv.textContent = '正在上传图片...';
  709. statusDiv.className = 'mt-2 text-sm text-blue-600';
  710. }
  711. try {
  712. const formData = new FormData();
  713. formData.append('file', file);
  714. const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
  715. method: 'POST',
  716. body: formData
  717. });
  718. if (!response.ok) {
  719. throw new Error(`上传失败: ${response.status} ${response.statusText}`);
  720. }
  721. const result = await response.json();
  722. let imageUrl = null;
  723. if (typeof result === 'string') {
  724. imageUrl = result;
  725. } else if (result.url) {
  726. imageUrl = result.url;
  727. } else if (result.data && result.data.url) {
  728. imageUrl = result.data.url;
  729. } else if (result.data && typeof result.data === 'string') {
  730. imageUrl = result.data;
  731. } else {
  732. const resultStr = JSON.stringify(result);
  733. const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
  734. if (urlMatch) {
  735. imageUrl = urlMatch[0];
  736. }
  737. }
  738. if (!imageUrl) {
  739. throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
  740. }
  741. const imageTag = `<image src="${imageUrl}"/>`;
  742. const cursorPos = stemTextarea.selectionStart;
  743. const textBefore = stemTextarea.value.substring(0, cursorPos);
  744. const textAfter = stemTextarea.value.substring(cursorPos);
  745. stemTextarea.value = textBefore + imageTag + textAfter;
  746. const newCursorPos = cursorPos + imageTag.length;
  747. stemTextarea.setSelectionRange(newCursorPos, newCursorPos);
  748. stemTextarea.focus();
  749. if (currentPreviewElement === stemTextarea) {
  750. updatePreviewContent(stemTextarea.value);
  751. }
  752. if (statusDiv) {
  753. statusDiv.textContent = '图片上传成功!已插入到题干中。';
  754. statusDiv.className = 'mt-2 text-sm text-green-600';
  755. setTimeout(() => {
  756. statusDiv.classList.add('hidden');
  757. }, 1500);
  758. }
  759. } catch (error) {
  760. console.error('上传失败:', error);
  761. if (statusDiv) {
  762. statusDiv.textContent = '上传失败: ' + error.message;
  763. statusDiv.className = 'mt-2 text-sm text-red-600';
  764. }
  765. if (window.customAlert) {
  766. window.customAlert('图片上传失败: ' + error.message);
  767. } else {
  768. alert('图片上传失败: ' + error.message);
  769. }
  770. } finally {
  771. stemTextarea.placeholder = originalPlaceholder;
  772. stemTextarea.style.opacity = '1';
  773. }
  774. }
  775. // 解析图片上传函数
  776. async function uploadSolutionImageFile(file) {
  777. const solutionTextarea = document.getElementById('solution-textarea');
  778. await uploadImageToInput(file, solutionTextarea);
  779. }
  780. // 通用图片上传函数:上传图片并插入到指定输入框
  781. async function uploadImageToInput(file, inputElement) {
  782. if (!inputElement || !file) {
  783. return;
  784. }
  785. if (!file.type.startsWith('image/')) {
  786. if (window.customAlert) {
  787. window.customAlert('请选择图片文件!');
  788. } else {
  789. alert('请选择图片文件!');
  790. }
  791. return;
  792. }
  793. const originalPlaceholder = inputElement.placeholder || '';
  794. const originalOpacity = inputElement.style.opacity || '1';
  795. inputElement.placeholder = '正在上传图片...';
  796. inputElement.style.opacity = '0.6';
  797. inputElement.disabled = true;
  798. try {
  799. const formData = new FormData();
  800. formData.append('file', file);
  801. const response = await fetch('https://crmapi.dcjxb.yunzhixue.cn/file/upload', {
  802. method: 'POST',
  803. body: formData
  804. });
  805. if (!response.ok) {
  806. throw new Error(`上传失败: ${response.status} ${response.statusText}`);
  807. }
  808. const result = await response.json();
  809. let imageUrl = null;
  810. if (typeof result === 'string') {
  811. imageUrl = result;
  812. } else if (result.url) {
  813. imageUrl = result.url;
  814. } else if (result.data && result.data.url) {
  815. imageUrl = result.data.url;
  816. } else if (result.data && typeof result.data === 'string') {
  817. imageUrl = result.data;
  818. } else {
  819. const resultStr = JSON.stringify(result);
  820. const urlMatch = resultStr.match(/https?:\/\/[^\s"']+/);
  821. if (urlMatch) {
  822. imageUrl = urlMatch[0];
  823. }
  824. }
  825. if (!imageUrl) {
  826. throw new Error('接口返回格式异常,未找到图片URL。返回结果:' + JSON.stringify(result));
  827. }
  828. const imageTag = `<image src="${imageUrl}"/>`;
  829. const cursorPos = inputElement.selectionStart || inputElement.value.length;
  830. const textBefore = inputElement.value.substring(0, cursorPos);
  831. const textAfter = inputElement.value.substring(cursorPos);
  832. inputElement.value = textBefore + imageTag + textAfter;
  833. const newCursorPos = cursorPos + imageTag.length;
  834. if (inputElement.setSelectionRange) {
  835. inputElement.setSelectionRange(newCursorPos, newCursorPos);
  836. }
  837. inputElement.focus();
  838. inputElement.dispatchEvent(new Event('input', { bubbles: true }));
  839. if (currentPreviewElement === inputElement) {
  840. updatePreviewContent(inputElement.value);
  841. }
  842. if (inputElement.id && inputElement.id.startsWith('option-')) {
  843. updateOptionsPreview();
  844. }
  845. } catch (error) {
  846. console.error('上传失败:', error);
  847. if (window.customAlert) {
  848. window.customAlert('图片上传失败: ' + error.message);
  849. } else {
  850. alert('图片上传失败: ' + error.message);
  851. }
  852. } finally {
  853. inputElement.placeholder = originalPlaceholder;
  854. inputElement.style.opacity = originalOpacity;
  855. inputElement.disabled = false;
  856. }
  857. }
  858. // 为所有输入框设置粘贴图片功能
  859. function setupPasteImageForAllInputs() {
  860. const allInputs = document.querySelectorAll('textarea, input[type="text"]');
  861. allInputs.forEach(input => {
  862. input.addEventListener('paste', async function(e) {
  863. const clipboardData = e.clipboardData || window.clipboardData;
  864. if (!clipboardData) {
  865. return;
  866. }
  867. const items = clipboardData.items;
  868. if (!items) {
  869. return;
  870. }
  871. for (let i = 0; i < items.length; i++) {
  872. const item = items[i];
  873. if (item.type.indexOf('image') !== -1) {
  874. e.preventDefault();
  875. const file = item.getAsFile();
  876. if (file) {
  877. await uploadImageToInput(file, input);
  878. }
  879. break;
  880. }
  881. }
  882. });
  883. });
  884. }
  885. // 页面加载时初始化
  886. document.addEventListener('DOMContentLoaded', function() {
  887. // 初始化难度选择框的回显
  888. const difficultySelect = document.getElementById('difficulty-select');
  889. if (difficultySelect) {
  890. const originalValue = difficultySelect.getAttribute('data-original');
  891. if (originalValue) {
  892. // 处理难度值的各种格式:可能是字符串 "0.2" 或数字 0.2
  893. let difficultyValue = originalValue;
  894. // 尝试转换为数字进行比较
  895. const numValue = parseFloat(originalValue);
  896. if (!isNaN(numValue)) {
  897. // 根据数值范围匹配对应的选项值
  898. if (numValue >= 0.19 && numValue <= 0.21) {
  899. difficultySelect.value = '0.2';
  900. } else if (numValue >= 0.39 && numValue <= 0.41) {
  901. difficultySelect.value = '0.4';
  902. } else if (numValue >= 0.69 && numValue <= 0.71) {
  903. difficultySelect.value = '0.7';
  904. } else if (originalValue === '0.2' || originalValue === 0.2) {
  905. difficultySelect.value = '0.2';
  906. } else if (originalValue === '0.4' || originalValue === 0.4) {
  907. difficultySelect.value = '0.4';
  908. } else if (originalValue === '0.7' || originalValue === 0.7) {
  909. difficultySelect.value = '0.7';
  910. }
  911. } else if (originalValue === '0.2' || originalValue === '0.4' || originalValue === '0.7') {
  912. difficultySelect.value = originalValue;
  913. }
  914. }
  915. }
  916. // 解析现有选项JSON并填充到输入框
  917. const optionsPreview = document.getElementById('options-preview');
  918. if (optionsPreview && optionsPreview.value && optionsPreview.value.trim() !== '{}') {
  919. try {
  920. const optionsObj = JSON.parse(optionsPreview.value);
  921. ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
  922. const input = document.getElementById(`option-${key}-input`);
  923. if (input && optionsObj[key]) {
  924. input.value = optionsObj[key];
  925. }
  926. });
  927. } catch (e) {
  928. console.warn('解析选项JSON失败:', e);
  929. }
  930. }
  931. updateOptionsPreview();
  932. // 题型选择变化
  933. const questionTypeSelect = document.getElementById('question-type-select');
  934. if (questionTypeSelect) {
  935. questionTypeSelect.addEventListener('change', function() {
  936. toggleOptionsVisibility(this.value);
  937. });
  938. // 初始化时根据题型显示/隐藏选项
  939. toggleOptionsVisibility(questionTypeSelect.value);
  940. }
  941. // 设置 LaTeX 实时预览
  942. setupPreviewForElement('stem-textarea', '题干预览');
  943. setupPreviewForElement('solution-textarea', '解析预览');
  944. setupPreviewForElement('option-A-input', '选项 A 预览');
  945. setupPreviewForElement('option-B-input', '选项 B 预览');
  946. setupPreviewForElement('option-C-input', '选项 C 预览');
  947. setupPreviewForElement('option-D-input', '选项 D 预览');
  948. setupPreviewForElement('answer-input', '正确答案预览');
  949. // 设置选项预览输入框的预览功能
  950. setupOptionsPreviewTextarea();
  951. // 为所有输入框添加粘贴图片功能
  952. setupPasteImageForAllInputs();
  953. });
  954. // 难度评价函数
  955. async function evaluateDifficulty() {
  956. const btn = document.getElementById('evaluate-difficulty-btn');
  957. const difficultySelect = document.getElementById('difficulty-select');
  958. if (!btn || !difficultySelect) return;
  959. // 收集题目信息
  960. const stemTextarea = document.getElementById('stem-textarea');
  961. const answerInput = document.getElementById('answer-input');
  962. const solutionTextarea = document.getElementById('solution-textarea');
  963. const optionsPreview = document.getElementById('options-preview');
  964. const questionTypeSelect = document.getElementById('question-type-select');
  965. const stem = stemTextarea ? stemTextarea.value.trim() : '';
  966. if (!stem) {
  967. if (window.customAlert) {
  968. window.customAlert('请先填写题干内容');
  969. } else {
  970. alert('请先填写题干内容');
  971. }
  972. return;
  973. }
  974. // 构建请求数据
  975. const requestData = {
  976. stem: stem,
  977. answer: answerInput ? answerInput.value.trim() : '',
  978. solution: solutionTextarea ? solutionTextarea.value.trim() : '',
  979. question_type: questionTypeSelect ? questionTypeSelect.value : ''
  980. };
  981. // 处理选项
  982. if (optionsPreview && optionsPreview.value.trim() && optionsPreview.value.trim() !== '{}') {
  983. try {
  984. requestData.options = JSON.parse(optionsPreview.value.trim());
  985. } catch (e) {
  986. console.warn('选项JSON解析失败:', e);
  987. }
  988. }
  989. // 显示加载状态
  990. const originalText = btn.innerHTML;
  991. btn.disabled = true;
  992. btn.innerHTML = '<svg class="w-3 h-3 inline-block mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>评价中...';
  993. try {
  994. const response = await fetch('/api/score', {
  995. method: 'POST',
  996. headers: {
  997. 'Content-Type': 'application/json'
  998. },
  999. body: JSON.stringify(requestData)
  1000. });
  1001. if (!response.ok) {
  1002. throw new Error(`请求失败: ${response.status} ${response.statusText}`);
  1003. }
  1004. const result = await response.json();
  1005. // 调试:打印完整返回结果
  1006. console.log('难度评价接口返回:', result);
  1007. // 处理返回的 difficulty_level(优先使用 data.difficulty_level,兼容旧格式)
  1008. let difficultyLevel = result.data?.difficulty_level || result.difficulty_level || result.difficulty || result.level;
  1009. // 映射难度等级到枚举值
  1010. let difficultyValue = '';
  1011. if (difficultyLevel !== undefined && difficultyLevel !== null) {
  1012. const levelStr = String(difficultyLevel).trim();
  1013. // 字符串匹配
  1014. if (levelStr === '筑基' || levelStr === '0.2' || levelStr === '0.20') {
  1015. difficultyValue = '0.2';
  1016. } else if (levelStr === '提分' || levelStr === '0.4' || levelStr === '0.40') {
  1017. difficultyValue = '0.4';
  1018. } else if (levelStr === '培优' || levelStr === '0.7' || levelStr === '0.70') {
  1019. difficultyValue = '0.7';
  1020. } else {
  1021. // 尝试转换为数字
  1022. const levelNum = parseFloat(levelStr);
  1023. if (!isNaN(levelNum)) {
  1024. if (Math.abs(levelNum - 0.2) < 0.1) {
  1025. difficultyValue = '0.2';
  1026. } else if (Math.abs(levelNum - 0.4) < 0.1) {
  1027. difficultyValue = '0.4';
  1028. } else if (Math.abs(levelNum - 0.7) < 0.1) {
  1029. difficultyValue = '0.7';
  1030. }
  1031. }
  1032. }
  1033. }
  1034. if (difficultyValue) {
  1035. difficultySelect.value = difficultyValue;
  1036. // 触发change事件以更新预览
  1037. difficultySelect.dispatchEvent(new Event('change', { bubbles: true }));
  1038. // 不显示弹窗,直接完成
  1039. } else {
  1040. // 如果无法识别,打印完整返回结果以便调试
  1041. console.error('无法识别难度等级,完整返回结果:', result);
  1042. if (window.customAlert) {
  1043. window.customAlert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
  1044. } else {
  1045. alert('无法识别返回的难度等级。返回数据:' + JSON.stringify(result));
  1046. }
  1047. }
  1048. } catch (error) {
  1049. console.error('难度评价失败:', error);
  1050. if (window.customAlert) {
  1051. window.customAlert('难度评价失败: ' + error.message);
  1052. } else {
  1053. alert('难度评价失败: ' + error.message);
  1054. }
  1055. } finally {
  1056. // 恢复按钮
  1057. btn.disabled = false;
  1058. btn.innerHTML = originalText;
  1059. }
  1060. }
  1061. // 表单提交
  1062. document.getElementById('edit-form').addEventListener('submit', async (e) => {
  1063. e.preventDefault();
  1064. const formData = new FormData(e.target);
  1065. // 只发送实际修改过的字段(对比原始值)
  1066. const data = { question_code: formData.get('question_code') };
  1067. // 检查字段变化
  1068. const fields = ['stem', 'answer', 'solution', 'question_type', 'difficulty'];
  1069. fields.forEach(field => {
  1070. const input = e.target.querySelector(`[name="${field}"]`);
  1071. if (input) {
  1072. const newValue = input.value || '';
  1073. let oldValue = input.getAttribute('data-original') || '';
  1074. // 处理 JSON 字符串
  1075. try {
  1076. if (oldValue && (oldValue.startsWith('{') || oldValue.startsWith('['))) {
  1077. oldValue = JSON.stringify(JSON.parse(oldValue));
  1078. }
  1079. } catch(e) {
  1080. // 解析失败,保持原值
  1081. }
  1082. // 处理difficulty字段:转换为浮点数,保留两位小数
  1083. if (field === 'difficulty' && newValue) {
  1084. const difficultyValue = parseFloat(newValue);
  1085. if (!isNaN(difficultyValue)) {
  1086. const newDifficulty = round(difficultyValue, 2);
  1087. const oldDifficulty = oldValue ? parseFloat(oldValue) : null;
  1088. if (newDifficulty !== oldDifficulty) {
  1089. data[field] = newDifficulty;
  1090. }
  1091. }
  1092. } else if (newValue !== oldValue) {
  1093. data[field] = newValue.trim();
  1094. }
  1095. }
  1096. });
  1097. // 处理选项
  1098. const previewTextarea = document.getElementById('options-preview');
  1099. const optionsOriginal = previewTextarea ? previewTextarea.getAttribute('data-original') || '' : '';
  1100. let optionsJson = '';
  1101. if (previewTextarea && previewTextarea.value.trim() && previewTextarea.value.trim() !== '{}') {
  1102. try {
  1103. const parsed = JSON.parse(previewTextarea.value.trim());
  1104. optionsJson = JSON.stringify(parsed);
  1105. } catch (e) {
  1106. const optionsObj = {};
  1107. ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
  1108. const input = document.getElementById(`option-${key}-input`);
  1109. if (input && input.value && input.value.trim()) {
  1110. optionsObj[key] = input.value.trim();
  1111. }
  1112. });
  1113. if (Object.keys(optionsObj).length > 0) {
  1114. optionsJson = JSON.stringify(optionsObj);
  1115. }
  1116. }
  1117. } else {
  1118. const optionsObj = {};
  1119. ['A', 'B', 'C', 'D', 'E', 'F'].forEach(key => {
  1120. const input = document.getElementById(`option-${key}-input`);
  1121. if (input && input.value && input.value.trim()) {
  1122. optionsObj[key] = input.value.trim();
  1123. }
  1124. });
  1125. if (Object.keys(optionsObj).length > 0) {
  1126. optionsJson = JSON.stringify(optionsObj);
  1127. }
  1128. }
  1129. // 对比选项是否变化
  1130. let optionsOriginalNormalized = optionsOriginal;
  1131. try {
  1132. if (optionsOriginal && (optionsOriginal.startsWith('{') || optionsOriginal.startsWith('['))) {
  1133. optionsOriginalNormalized = JSON.stringify(JSON.parse(optionsOriginal));
  1134. }
  1135. } catch(e) {
  1136. // 解析失败,保持原值
  1137. }
  1138. if (optionsJson && optionsJson !== optionsOriginalNormalized) {
  1139. data.options = optionsJson;
  1140. } else if (!optionsJson && optionsOriginalNormalized) {
  1141. // 如果新值为空但原值不为空,也要更新(删除选项)
  1142. data.options = '';
  1143. }
  1144. // 验证必填项
  1145. const requiredFields = {
  1146. 'stem': '题干',
  1147. 'answer': '正确答案',
  1148. 'solution': '解析'
  1149. };
  1150. const missingFields = [];
  1151. for (const [field, label] of Object.entries(requiredFields)) {
  1152. const input = e.target.querySelector(`[name="${field}"]`);
  1153. if (!input || !input.value || !input.value.trim()) {
  1154. missingFields.push(label);
  1155. }
  1156. }
  1157. if (missingFields.length > 0) {
  1158. const message = '请填写以下必填项:' + missingFields.join('、');
  1159. if (window.customAlert) {
  1160. window.customAlert(message);
  1161. } else {
  1162. alert(message);
  1163. }
  1164. return;
  1165. }
  1166. // 如果没有变化,提示用户
  1167. if (Object.keys(data).length === 1) {
  1168. if (window.customAlert) {
  1169. window.customAlert('没有检测到任何修改');
  1170. } else {
  1171. alert('没有检测到任何修改');
  1172. }
  1173. return;
  1174. }
  1175. const res = await fetch('/update_question', {
  1176. method: 'POST',
  1177. headers: {'Content-Type': 'application/json'},
  1178. body: JSON.stringify(data)
  1179. });
  1180. const result = await res.json();
  1181. if(result.success) {
  1182. if (window.customAlert) {
  1183. window.customAlert('修改成功!', () => {
  1184. window.location.href = '/detail/' + data.question_code;
  1185. });
  1186. } else {
  1187. alert('修改成功!');
  1188. window.location.href = '/detail/' + data.question_code;
  1189. }
  1190. } else {
  1191. if (window.customAlert) {
  1192. window.customAlert('修改失败: ' + result.error);
  1193. } else {
  1194. alert('修改失败: ' + result.error);
  1195. }
  1196. }
  1197. });
  1198. </script>
  1199. {% endblock %}