audit_questions.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. {% extends "layout.html" %}
  2. {% block page_title %}审核题目{% endblock %}
  3. {% block content %}
  4. <div class="flex gap-6">
  5. <!-- 左侧目录索引 -->
  6. <div class="w-96 flex-shrink-0">
  7. <div class="apple-card p-6 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
  8. <div class="flex items-center justify-between mb-6 pb-3 border-b border-gray-200">
  9. <h2 class="text-lg font-bold text-gray-800">未审核知识点</h2>
  10. </div>
  11. <nav class="space-y-1">
  12. {% macro render_kp_node(node, level) %}
  13. <div class="kp-node-item mb-1" data-kp-code="{{ node.kp_code }}" data-level="{{ level }}">
  14. <div class="flex items-center gap-1 group">
  15. {% if node.children|length > 0 %}
  16. <button
  17. onclick="toggleKpNode('{{ node.kp_code }}'); event.stopPropagation();"
  18. class="w-6 h-6 rounded flex items-center justify-center text-gray-500 hover:text-orange-600 hover:bg-orange-50 transition-all flex-shrink-0 kp-expand-btn"
  19. data-expanded="{% if level == 0 %}true{% else %}false{% endif %}"
  20. data-kp-code="{{ node.kp_code }}">
  21. {% if level == 0 %}
  22. <i class="ri-subtract-line text-sm"></i>
  23. {% else %}
  24. <i class="ri-add-line text-sm"></i>
  25. {% endif %}
  26. </button>
  27. {% else %}
  28. <div class="w-6 h-6 flex items-center justify-center flex-shrink-0">
  29. <div class="w-1.5 h-1.5 rounded-full bg-gray-400"></div>
  30. </div>
  31. {% endif %}
  32. <a href="javascript:void(0)"
  33. onclick="loadPendingQuestionsByKp('{{ node.kp_code }}', '{{ node.name }}', this); return false;"
  34. data-kp-code="{{ node.kp_code }}"
  35. data-kp-name="{{ node.name }}"
  36. class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-orange-50 hover:to-amber-50 transition-all border-l-2 border-transparent hover:border-orange-400 kp-link">
  37. <div class="flex items-center gap-2 flex-nowrap">
  38. {% if level == 0 %}
  39. <span class="bg-gradient-to-r from-slate-600 to-slate-700 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">章</span>
  40. {% elif level == 1 %}
  41. <span class="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">节</span>
  42. {% else %}
  43. <span class="bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded text-xs font-medium flex-shrink-0">小节</span>
  44. {% endif %}
  45. <span class="text-sm {% if level == 0 %}font-bold text-gray-800{% elif level == 1 %}font-semibold text-gray-800{% else %}font-normal text-gray-600{% endif %} group-hover:text-orange-600 transition-colors flex-1 min-w-0 truncate">{{ node.name }}</span>
  46. {% set pending_count = node.total_pending_count|default(node.pending_count|default(0)) %}
  47. {% if pending_count > 0 %}
  48. <span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ pending_count }}</span>
  49. {% endif %}
  50. </div>
  51. </a>
  52. </div>
  53. {% if node.children|length > 0 %}
  54. <div class="kp-children ml-8 mt-1 {% if level >= 1 %}hidden{% endif %}" id="kp-children-{{ node.kp_code }}" data-level="{{ level }}">
  55. {% for child in node.children %}
  56. {{ render_kp_node(child, level + 1) }}
  57. {% endfor %}
  58. </div>
  59. {% endif %}
  60. </div>
  61. {% endmacro %}
  62. <!-- 其他题目节点 -->
  63. {% if other_questions_count|default(0) > 0 %}
  64. <div class="kp-node-item mb-2 pb-2 border-b border-gray-200" data-kp-code="null" data-level="-1">
  65. <a href="javascript:void(0)"
  66. onclick="loadPendingQuestionsByKp('null', '其他题目', this); return false;"
  67. data-kp-code="null"
  68. data-kp-name="其他题目"
  69. class="flex-1 block px-3 py-2 rounded-lg hover:bg-gradient-to-r hover:from-purple-50 hover:to-pink-50 transition-all border-l-2 border-transparent hover:border-purple-400 kp-link">
  70. <div class="flex items-center gap-2 flex-nowrap">
  71. <span class="bg-gradient-to-r from-purple-500 to-pink-500 text-white px-2 py-0.5 rounded text-xs font-bold shadow-sm flex-shrink-0">其他</span>
  72. <span class="text-sm font-bold text-gray-800 group-hover:text-purple-600 transition-colors flex-1 min-w-0 truncate">其他题目</span>
  73. <span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full text-xs font-bold flex-shrink-0 ml-1">{{ other_questions_count }}</span>
  74. </div>
  75. </a>
  76. </div>
  77. {% endif %}
  78. {% if kp_tree %}
  79. {% for root_node in kp_tree %}
  80. {{ render_kp_node(root_node, 0) }}
  81. {% endfor %}
  82. {% else %}
  83. <div class="text-sm text-gray-500 text-center py-8">暂无未审核题目</div>
  84. {% endif %}
  85. </nav>
  86. </div>
  87. </div>
  88. <!-- 右侧题目列表区域 -->
  89. <div class="flex-1">
  90. <div id="questions-container" class="space-y-4">
  91. <div class="apple-card p-12 text-center">
  92. <div class="text-gray-400 mb-4">
  93. <i class="ri-file-check-line text-6xl"></i>
  94. </div>
  95. <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点</h3>
  96. <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的未审核题目</p>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. <script>
  102. // 知识点目录折叠/展开功能
  103. function toggleKpNode(kpCode) {
  104. const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
  105. const expandBtn = document.querySelector(`.kp-expand-btn[data-kp-code="${kpCode}"]`);
  106. if (!childrenContainer || !expandBtn) return;
  107. const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
  108. const icon = expandBtn.querySelector('i');
  109. if (isExpanded) {
  110. // 折叠
  111. childrenContainer.classList.add('hidden');
  112. expandBtn.setAttribute('data-expanded', 'false');
  113. if (icon) {
  114. icon.className = 'ri-add-line text-sm';
  115. }
  116. } else {
  117. // 展开
  118. childrenContainer.classList.remove('hidden');
  119. expandBtn.setAttribute('data-expanded', 'true');
  120. if (icon) {
  121. icon.className = 'ri-subtract-line text-sm';
  122. }
  123. }
  124. }
  125. // 存储当前知识点的代码和名称
  126. let currentKpCode = null;
  127. let currentKpName = null;
  128. // 加载指定知识点的未审核题目列表
  129. function loadPendingQuestionsByKp(kpCode, kpName, linkElement) {
  130. const container = document.getElementById('questions-container');
  131. if (!container) return;
  132. // 保存当前知识点信息
  133. currentKpCode = kpCode;
  134. currentKpName = kpName;
  135. // 更新选中状态
  136. document.querySelectorAll('.kp-link').forEach(link => {
  137. link.classList.remove('bg-orange-50', 'border-orange-400');
  138. });
  139. if (linkElement) {
  140. linkElement.classList.add('bg-orange-50', 'border-orange-400');
  141. }
  142. // 显示加载状态
  143. container.innerHTML = `
  144. <div class="apple-card p-12 text-center">
  145. <div class="text-orange-500 mb-4">
  146. <i class="ri-loader-4-line text-6xl animate-spin"></i>
  147. </div>
  148. <p class="text-gray-600">正在加载未审核题目...</p>
  149. </div>
  150. `;
  151. // 请求未审核题目列表
  152. fetch(`/api/pending_questions_by_kp/${encodeURIComponent(kpCode)}`)
  153. .then(response => response.json())
  154. .then(data => {
  155. if (!data.success) {
  156. throw new Error(data.error || '加载失败');
  157. }
  158. const questions = data.questions || [];
  159. const kpName = data.kp_name || kpCode;
  160. // 如果没有题目,显示提示信息
  161. if (questions.length === 0) {
  162. container.innerHTML = `
  163. <div class="apple-card p-12 text-center">
  164. <div class="text-gray-400 mb-4">
  165. <i class="ri-file-list-line text-6xl"></i>
  166. </div>
  167. <h3 class="text-lg font-bold text-gray-600 mb-2">该知识点下暂无未审核题目</h3>
  168. <p class="text-sm text-gray-500">所有题目已审核完成</p>
  169. </div>
  170. `;
  171. return;
  172. }
  173. // 有题目时,渲染题目列表
  174. let html = `<div class="mb-4 flex items-center justify-between">
  175. <h3 class="text-xl font-bold text-gray-800">${kpName} <span class="text-sm font-normal text-gray-500">(${questions.length} 题待审核)</span></h3>
  176. </div>`;
  177. questions.forEach(q => {
  178. // 审核状态标签(应该都是待审核)
  179. let auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
  180. // 难度标签
  181. let difficultyBadge = '';
  182. if (q.difficulty !== null && q.difficulty !== undefined) {
  183. const diff = parseFloat(q.difficulty);
  184. if (diff === 0.2 || diff === 0.4) {
  185. difficultyBadge = '<span class="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">筑基</span>';
  186. } else if (diff === 0.4 || diff === 0.6) {
  187. difficultyBadge = '<span class="bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">提分</span>';
  188. } else if (diff === 0.7 || diff === 0.8) {
  189. difficultyBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">培优</span>';
  190. }
  191. }
  192. // 题目类型标签
  193. let typeBadge = '';
  194. if (q.question_type) {
  195. const typeMap = {
  196. 'single': '单选题',
  197. 'multiple': '多选题',
  198. 'judge': '判断题',
  199. 'fill': '填空题',
  200. 'essay': '解答题'
  201. };
  202. const typeName = typeMap[q.question_type] || q.question_type;
  203. typeBadge = `<span class="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs font-medium whitespace-nowrap">${typeName}</span>`;
  204. }
  205. // 题干预览(限制长度)
  206. const stemPreview = q.stem ? (q.stem.length > 150 ? q.stem.substring(0, 150) + '...' : q.stem) : '无题干';
  207. html += `
  208. <div class="apple-card p-6 flex items-center justify-between hover:shadow-lg transition-all">
  209. <div class="flex-1 pr-8">
  210. <div class="flex items-center space-x-3 mb-2 flex-wrap gap-2">
  211. <span class="text-xs font-bold uppercase tracking-wider text-gray-400">#${q.question_code}</span>
  212. ${auditBadge}
  213. ${difficultyBadge}
  214. ${typeBadge}
  215. </div>
  216. <p class="text-gray-700 line-clamp-2 math-render">${stemPreview}</p>
  217. </div>
  218. <a href="/detail/${q.question_code}" class="btn-apple bg-gradient-to-r from-orange-500 to-orange-600 text-white hover:from-orange-600 hover:to-orange-700 shadow-lg shadow-orange-200 flex items-center gap-2 px-4 py-2.5 whitespace-nowrap">
  219. <i class="ri-file-check-line"></i>
  220. <span>去审核</span>
  221. </a>
  222. </div>
  223. `;
  224. });
  225. container.innerHTML = html;
  226. // 渲染数学公式
  227. if (window.renderMathInElement) {
  228. container.querySelectorAll('.math-render').forEach(el => {
  229. try {
  230. window.renderMathInElement(el, {
  231. delimiters: [
  232. {left: "$$", right: "$$", display: true},
  233. {left: "$", right: "$", display: false},
  234. {left: "\\(", right: "\\)", display: false},
  235. {left: "\\[", right: "\\]", display: true}
  236. ],
  237. throwOnError: false
  238. });
  239. } catch (e) {
  240. console.warn("数学公式渲染失败:", e);
  241. }
  242. });
  243. }
  244. })
  245. .catch(err => {
  246. container.innerHTML = `
  247. <div class="apple-card p-12 text-center">
  248. <div class="text-red-500 mb-4">
  249. <i class="ri-error-warning-line text-6xl"></i>
  250. </div>
  251. <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
  252. <p class="text-sm text-gray-500">${err.message || '未知错误'}</p>
  253. </div>
  254. `;
  255. });
  256. }
  257. // 页面加载完成后,初始化知识点目录
  258. document.addEventListener('DOMContentLoaded', function() {
  259. // 默认展开第一级节点
  260. const firstLevelNodes = document.querySelectorAll('.kp-node-item[data-level="0"]');
  261. firstLevelNodes.forEach(node => {
  262. const kpCode = node.getAttribute('data-kp-code');
  263. const expandBtn = node.querySelector('.kp-expand-btn');
  264. if (expandBtn && expandBtn.getAttribute('data-expanded') === 'true') {
  265. const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
  266. if (childrenContainer) {
  267. childrenContainer.classList.remove('hidden');
  268. }
  269. }
  270. });
  271. });
  272. </script>
  273. <style>
  274. .line-clamp-2 {
  275. display: -webkit-box;
  276. -webkit-line-clamp: 2;
  277. -webkit-box-orient: vertical;
  278. overflow: hidden;
  279. }
  280. </style>
  281. {% endblock %}