audit_questions.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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, page = null) {
  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. // 如果没有指定页码,从URL参数获取
  152. if (page === null) {
  153. const urlParams = new URLSearchParams(window.location.search);
  154. const pageParam = urlParams.get('page');
  155. page = pageParam ? parseInt(pageParam) : 1;
  156. }
  157. // 构建请求URL(添加分页参数)
  158. let apiUrl = `/api/pending_questions_by_kp/${encodeURIComponent(kpCode)}`;
  159. if (page > 1) {
  160. apiUrl += `?page=${page}`;
  161. }
  162. // 请求未审核题目列表
  163. fetch(apiUrl)
  164. .then(response => response.json())
  165. .then(data => {
  166. if (!data.success) {
  167. throw new Error(data.error || '加载失败');
  168. }
  169. const questions = data.questions || [];
  170. const kpName = data.kp_name || kpCode;
  171. // 获取分页信息(从API返回)
  172. const pagination = data.pagination || {};
  173. const currentPage = pagination.page || 1;
  174. const totalPages = pagination.total_pages || 1;
  175. const totalCount = pagination.total_count || questions.length;
  176. const pageSize = pagination.page_size || 20;
  177. const startIndex = (currentPage - 1) * pageSize;
  178. const endIndex = Math.min(startIndex + pageSize, totalCount);
  179. // 渲染题目列表
  180. let html = '';
  181. html += `<div class="space-y-3 ${kpCode === 'null' || kpCode === '' ? '' : 'mt-6'}">`;
  182. // 如果没有题目,显示提示信息
  183. if (questions.length === 0) {
  184. html += `
  185. <div class="apple-card p-12 text-center">
  186. <div class="text-gray-400 mb-4">
  187. <i class="ri-file-list-line text-6xl"></i>
  188. </div>
  189. <h3 class="text-lg font-bold text-gray-600 mb-2">该知识点下暂无未审核题目</h3>
  190. <p class="text-sm text-gray-500">所有题目已审核完成</p>
  191. </div>
  192. `;
  193. } else {
  194. // 有题目时,渲染题目列表(完全按照题目管理页面的格式)
  195. questions.forEach(q => {
  196. // 审核状态(待审核)
  197. let auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
  198. // 难度标签
  199. let difficultyBadge = '';
  200. if (q.difficulty !== null && q.difficulty !== undefined) {
  201. const diff = parseFloat(q.difficulty);
  202. if (diff === 0.2 || Math.abs(diff - 0.2) < 0.1) {
  203. difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-green-100 text-green-700 border border-green-200 whitespace-nowrap">筑基</span>';
  204. } else if (diff === 0.4 || Math.abs(diff - 0.4) < 0.1) {
  205. difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-yellow-100 text-yellow-700 border border-yellow-200 whitespace-nowrap">提分</span>';
  206. } else if (diff === 0.7 || Math.abs(diff - 0.7) < 0.1) {
  207. difficultyBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-orange-100 text-orange-700 border border-orange-200 whitespace-nowrap">培优</span>';
  208. }
  209. }
  210. // 题型标签
  211. const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
  212. const questionTypeText = typeMap[q.question_type] || q.question_type || '未分类';
  213. // 年级标签
  214. let gradeBadge = '';
  215. if (q.grade !== null && q.grade !== undefined) {
  216. const grade = parseInt(q.grade);
  217. if (grade === 1) {
  218. gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-pink-100 text-pink-700 border border-pink-200 whitespace-nowrap">小学</span>';
  219. } else if (grade === 2) {
  220. gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-blue-100 text-blue-700 border border-blue-200 whitespace-nowrap">初中</span>';
  221. } else if (grade === 3) {
  222. gradeBadge = '<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-purple-100 text-purple-700 border border-purple-200 whitespace-nowrap">高中</span>';
  223. }
  224. }
  225. // 题干预览(去除HTML标签,只显示文本)
  226. const stemText = (q.stem || '').replace(/<[^>]*>/g, '').substring(0, 150);
  227. html += `
  228. <a href="/detail/${q.question_code}${currentKpCode ? '?kp_code=' + encodeURIComponent(currentKpCode) : ''}"
  229. class="apple-card p-4 block group hover:shadow-lg transition-all border-l-4 border-transparent hover:border-blue-500">
  230. <div class="flex items-center gap-4">
  231. <!-- 左侧:题号 -->
  232. <div class="flex-shrink-0">
  233. <span class="text-xs font-mono text-gray-500 bg-gray-100 px-3 py-1.5 rounded font-semibold">${q.question_code}</span>
  234. </div>
  235. <!-- 中间:题干内容 -->
  236. <div class="flex-1 min-w-0">
  237. <div class="text-sm text-gray-800 group-hover:text-blue-600 transition-colors line-clamp-1">
  238. ${stemText}${stemText.length >= 150 ? '...' : ''}
  239. </div>
  240. </div>
  241. <!-- 右侧:标签和操作 -->
  242. <div class="flex items-center gap-3 flex-shrink-0">
  243. <span class="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">${questionTypeText}</span>
  244. ${gradeBadge}
  245. ${difficultyBadge}
  246. ${auditBadge}
  247. <span class="text-xs text-blue-600 font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">查看详情 →</span>
  248. </div>
  249. </div>
  250. </a>
  251. `;
  252. });
  253. // 添加翻页控件(完全按照题目管理页面的格式)
  254. if (totalPages > 1) {
  255. html += `
  256. <div class="mt-6 flex items-center justify-between">
  257. <div class="text-sm text-gray-600">
  258. 显示第 ${startIndex + 1}-${endIndex} 题,共 ${totalCount} 题
  259. </div>
  260. <div class="flex items-center gap-2">
  261. <button
  262. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${currentPage - 1})"
  263. ${currentPage === 1 ? 'disabled' : ''}
  264. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  265. currentPage === 1
  266. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  267. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  268. }">
  269. <i class="ri-arrow-left-s-line"></i> 上一页
  270. </button>
  271. <div class="flex items-center gap-1">
  272. ${Array.from({length: totalPages}, (_, i) => i + 1).map(page => {
  273. if (page === 1 || page === totalPages || (page >= currentPage - 2 && page <= currentPage + 2)) {
  274. return `
  275. <button
  276. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${page})"
  277. class="px-3 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  278. page === currentPage
  279. ? 'border-blue-500 bg-blue-500 text-white'
  280. : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
  281. }">
  282. ${page}
  283. </button>
  284. `;
  285. } else if (page === currentPage - 3 || page === currentPage + 3) {
  286. return '<span class="px-2 text-gray-400">...</span>';
  287. }
  288. return '';
  289. }).join('')}
  290. </div>
  291. <button
  292. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${currentPage + 1})"
  293. ${currentPage === totalPages ? 'disabled' : ''}
  294. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  295. currentPage === totalPages
  296. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  297. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  298. }">
  299. 下一页 <i class="ri-arrow-right-s-line"></i>
  300. </button>
  301. </div>
  302. </div>
  303. `;
  304. }
  305. }
  306. html += '</div>';
  307. container.innerHTML = html;
  308. // 渲染数学公式
  309. if (window.renderMathInElement) {
  310. container.querySelectorAll('.math-render').forEach(el => {
  311. try {
  312. window.renderMathInElement(el, {
  313. delimiters: [
  314. {left: "$$", right: "$$", display: true},
  315. {left: "$", right: "$", display: false},
  316. {left: "\\(", right: "\\)", display: false},
  317. {left: "\\[", right: "\\]", display: true}
  318. ],
  319. throwOnError: false
  320. });
  321. } catch (e) {
  322. console.warn('数学公式渲染失败:', e);
  323. }
  324. });
  325. }
  326. })
  327. .catch(error => {
  328. container.innerHTML = `
  329. <div class="apple-card p-12 text-center">
  330. <div class="text-red-400 mb-4">
  331. <i class="ri-error-warning-line text-6xl"></i>
  332. </div>
  333. <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
  334. <p class="text-sm text-red-500">${error.message || '未知错误'}</p>
  335. </div>
  336. `;
  337. });
  338. }
  339. // 分页加载函数(完全按照题目管理页面的格式)
  340. function loadPendingQuestionsByKpPage(kpCode, page) {
  341. // 更新URL参数,但不刷新页面
  342. const url = new URL(window.location);
  343. url.searchParams.set('kp_code', kpCode);
  344. if (page > 1) {
  345. url.searchParams.set('page', page);
  346. } else {
  347. url.searchParams.delete('page');
  348. }
  349. window.history.pushState({}, '', url);
  350. // 重新加载题目列表
  351. const linkElement = document.querySelector(`.kp-link[data-kp-code="${kpCode}"]`);
  352. const kpName = linkElement ? (linkElement.getAttribute('data-kp-name') || kpCode) : kpCode;
  353. loadPendingQuestionsByKp(kpCode, kpName, linkElement, page);
  354. }
  355. // 页面加载完成后,初始化知识点目录
  356. document.addEventListener('DOMContentLoaded', function() {
  357. // 默认展开第一级节点
  358. const firstLevelNodes = document.querySelectorAll('.kp-node-item[data-level="0"]');
  359. firstLevelNodes.forEach(node => {
  360. const kpCode = node.getAttribute('data-kp-code');
  361. const expandBtn = node.querySelector('.kp-expand-btn');
  362. if (expandBtn && expandBtn.getAttribute('data-expanded') === 'true') {
  363. const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
  364. if (childrenContainer) {
  365. childrenContainer.classList.remove('hidden');
  366. }
  367. }
  368. });
  369. });
  370. </script>
  371. {% endblock %}