audit_questions.html 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  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. <!-- 筛选栏 -->
  91. <div id="filter-bar" class="apple-card p-4 mb-4">
  92. <div class="flex items-center gap-4 flex-wrap">
  93. <span class="text-sm font-medium text-gray-700">筛选:</span>
  94. <!-- 创建人筛选 -->
  95. <div class="flex items-center gap-2">
  96. <label class="text-sm text-gray-600">创建人:</label>
  97. <select id="filter-create-by" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
  98. <option value="">全部</option>
  99. {% for creator in creators|default([]) %}
  100. <option value="{{ creator }}">{{ creator }}</option>
  101. {% endfor %}
  102. </select>
  103. </div>
  104. <!-- 创建时间筛选 -->
  105. <div class="flex items-center gap-2">
  106. <label class="text-sm text-gray-600">创建时间:</label>
  107. <input type="date" id="filter-created-at" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
  108. </div>
  109. <!-- 重置按钮 -->
  110. <button onclick="resetFilters()" class="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 transition-colors">
  111. 重置
  112. </button>
  113. </div>
  114. </div>
  115. <div id="questions-container" class="space-y-4">
  116. <div class="apple-card p-12 text-center">
  117. <div class="text-gray-400 mb-4">
  118. <i class="ri-file-check-line text-6xl"></i>
  119. </div>
  120. <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点或设置筛选条件</h3>
  121. <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的未审核题目,或使用筛选条件查看所有题目</p>
  122. </div>
  123. </div>
  124. </div>
  125. </div>
  126. <script>
  127. // 知识点目录折叠/展开功能
  128. function toggleKpNode(kpCode) {
  129. const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
  130. const expandBtn = document.querySelector(`.kp-expand-btn[data-kp-code="${kpCode}"]`);
  131. if (!childrenContainer || !expandBtn) return;
  132. const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
  133. const icon = expandBtn.querySelector('i');
  134. if (isExpanded) {
  135. // 折叠
  136. childrenContainer.classList.add('hidden');
  137. expandBtn.setAttribute('data-expanded', 'false');
  138. if (icon) {
  139. icon.className = 'ri-add-line text-sm';
  140. }
  141. } else {
  142. // 展开
  143. childrenContainer.classList.remove('hidden');
  144. expandBtn.setAttribute('data-expanded', 'true');
  145. if (icon) {
  146. icon.className = 'ri-subtract-line text-sm';
  147. }
  148. }
  149. }
  150. // 存储当前知识点的代码和名称
  151. let currentKpCode = null;
  152. let currentKpName = null;
  153. // 加载指定知识点的未审核题目列表(支持分页和筛选)
  154. function loadPendingQuestionsByKp(kpCode, kpName, linkElement, page = null) {
  155. const container = document.getElementById('questions-container');
  156. if (!container) return;
  157. // 保存当前知识点信息
  158. currentKpCode = kpCode;
  159. currentKpName = kpName;
  160. // 更新选中状态
  161. document.querySelectorAll('.kp-link').forEach(link => {
  162. link.classList.remove('bg-orange-50', 'border-orange-400');
  163. });
  164. if (linkElement) {
  165. linkElement.classList.add('bg-orange-50', 'border-orange-400');
  166. }
  167. // 显示加载状态
  168. container.innerHTML = `
  169. <div class="apple-card p-12 text-center">
  170. <div class="text-orange-500 mb-4">
  171. <i class="ri-loader-4-line text-6xl animate-spin"></i>
  172. </div>
  173. <p class="text-gray-600">正在加载未审核题目...</p>
  174. </div>
  175. `;
  176. // 如果没有指定页码,从URL参数获取
  177. if (page === null) {
  178. const urlParams = new URLSearchParams(window.location.search);
  179. const pageParam = urlParams.get('page');
  180. page = pageParam ? parseInt(pageParam) : 1;
  181. }
  182. // 获取筛选参数
  183. const createByFilter = document.getElementById('filter-create-by')?.value || '';
  184. const createdAtFilter = document.getElementById('filter-created-at')?.value || '';
  185. // 构建请求URL(添加分页和筛选参数)
  186. let apiUrl = `/api/pending_questions_by_kp/${encodeURIComponent(kpCode)}`;
  187. const params = new URLSearchParams();
  188. if (page > 1) {
  189. params.append('page', page);
  190. }
  191. if (createByFilter) {
  192. params.append('create_by', createByFilter);
  193. }
  194. if (createdAtFilter) {
  195. params.append('created_at', createdAtFilter);
  196. }
  197. if (params.toString()) {
  198. apiUrl += '?' + params.toString();
  199. }
  200. // 请求未审核题目列表
  201. fetch(apiUrl)
  202. .then(response => response.json())
  203. .then(data => {
  204. if (!data.success) {
  205. throw new Error(data.error || '加载失败');
  206. }
  207. const questions = data.questions || [];
  208. const kpName = data.kp_name || kpCode;
  209. // 获取分页信息(从API返回)
  210. const pagination = data.pagination || {};
  211. const currentPage = pagination.page || 1;
  212. const totalPages = pagination.total_pages || 1;
  213. const totalCount = pagination.total_count || questions.length;
  214. const pageSize = pagination.page_size || 20;
  215. const startIndex = (currentPage - 1) * pageSize;
  216. const endIndex = Math.min(startIndex + pageSize, totalCount);
  217. // 渲染题目列表
  218. let html = '';
  219. html += `<div class="space-y-3 ${kpCode === 'null' || kpCode === '' ? '' : 'mt-6'}">`;
  220. // 如果没有题目,显示提示信息
  221. if (questions.length === 0) {
  222. html += `
  223. <div class="apple-card p-12 text-center">
  224. <div class="text-gray-400 mb-4">
  225. <i class="ri-file-list-line text-6xl"></i>
  226. </div>
  227. <h3 class="text-lg font-bold text-gray-600 mb-2">该知识点下暂无未审核题目</h3>
  228. <p class="text-sm text-gray-500">所有题目已审核完成</p>
  229. </div>
  230. `;
  231. } else {
  232. // 有题目时,渲染题目列表(完全按照题目管理页面的格式)
  233. questions.forEach(q => {
  234. // 审核状态(待审核)
  235. let auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
  236. // 难度标签
  237. let difficultyBadge = '';
  238. if (q.difficulty !== null && q.difficulty !== undefined) {
  239. const diff = parseFloat(q.difficulty);
  240. if (diff === 0.2 || Math.abs(diff - 0.2) < 0.1) {
  241. 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>';
  242. } else if (diff === 0.4 || Math.abs(diff - 0.4) < 0.1) {
  243. 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>';
  244. } else if (diff === 0.7 || Math.abs(diff - 0.7) < 0.1) {
  245. 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>';
  246. }
  247. }
  248. // 题型标签
  249. const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
  250. const questionTypeText = typeMap[q.question_type] || q.question_type || '未分类';
  251. // 年级标签
  252. let gradeBadge = '';
  253. if (q.grade !== null && q.grade !== undefined) {
  254. const grade = parseInt(q.grade);
  255. if (grade === 1) {
  256. 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>';
  257. } else if (grade === 2) {
  258. 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>';
  259. } else if (grade === 3) {
  260. 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>';
  261. }
  262. }
  263. // 题干预览(去除HTML标签,只显示文本)
  264. const stemText = (q.stem || '').replace(/<[^>]*>/g, '').substring(0, 150);
  265. html += `
  266. <a href="/detail/${q.question_code}${currentKpCode ? '?kp_code=' + encodeURIComponent(currentKpCode) : ''}"
  267. class="apple-card p-4 block group hover:shadow-lg transition-all border-l-4 border-transparent hover:border-blue-500">
  268. <div class="flex items-center gap-4">
  269. <!-- 左侧:题号 -->
  270. <div class="flex-shrink-0">
  271. <span class="text-xs font-mono text-gray-500 bg-gray-100 px-3 py-1.5 rounded font-semibold">${q.question_code}</span>
  272. </div>
  273. <!-- 中间:题干内容 -->
  274. <div class="flex-1 min-w-0">
  275. <div class="text-sm text-gray-800 group-hover:text-blue-600 transition-colors line-clamp-1">
  276. ${stemText}${stemText.length >= 150 ? '...' : ''}
  277. </div>
  278. </div>
  279. <!-- 右侧:标签和操作 -->
  280. <div class="flex items-center gap-3 flex-shrink-0">
  281. <span class="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">${questionTypeText}</span>
  282. ${gradeBadge}
  283. ${difficultyBadge}
  284. ${auditBadge}
  285. <span class="text-xs text-blue-600 font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">查看详情 →</span>
  286. </div>
  287. </div>
  288. </a>
  289. `;
  290. });
  291. // 添加翻页控件(完全按照题目管理页面的格式)
  292. if (totalPages > 1) {
  293. html += `
  294. <div class="mt-6 flex items-center justify-between">
  295. <div class="text-sm text-gray-600">
  296. 显示第 ${startIndex + 1}-${endIndex} 题,共 ${totalCount} 题
  297. </div>
  298. <div class="flex items-center gap-2">
  299. <button
  300. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${currentPage - 1})"
  301. ${currentPage === 1 ? 'disabled' : ''}
  302. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  303. currentPage === 1
  304. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  305. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  306. }">
  307. <i class="ri-arrow-left-s-line"></i> 上一页
  308. </button>
  309. <div class="flex items-center gap-1">
  310. ${Array.from({length: totalPages}, (_, i) => i + 1).map(page => {
  311. if (page === 1 || page === totalPages || (page >= currentPage - 2 && page <= currentPage + 2)) {
  312. return `
  313. <button
  314. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${page})"
  315. class="px-3 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  316. page === currentPage
  317. ? 'border-blue-500 bg-blue-500 text-white'
  318. : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
  319. }">
  320. ${page}
  321. </button>
  322. `;
  323. } else if (page === currentPage - 3 || page === currentPage + 3) {
  324. return '<span class="px-2 text-gray-400">...</span>';
  325. }
  326. return '';
  327. }).join('')}
  328. </div>
  329. <button
  330. onclick="loadPendingQuestionsByKpPage('${kpCode}', ${currentPage + 1})"
  331. ${currentPage === totalPages ? 'disabled' : ''}
  332. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  333. currentPage === totalPages
  334. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  335. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  336. }">
  337. 下一页 <i class="ri-arrow-right-s-line"></i>
  338. </button>
  339. </div>
  340. </div>
  341. `;
  342. }
  343. }
  344. html += '</div>';
  345. container.innerHTML = html;
  346. // 渲染数学公式
  347. if (window.renderMathInElement) {
  348. container.querySelectorAll('.math-render').forEach(el => {
  349. try {
  350. window.renderMathInElement(el, {
  351. delimiters: [
  352. {left: "$$", right: "$$", display: true},
  353. {left: "$", right: "$", display: false},
  354. {left: "\\(", right: "\\)", display: false},
  355. {left: "\\[", right: "\\]", display: true}
  356. ],
  357. throwOnError: false
  358. });
  359. } catch (e) {
  360. console.warn('数学公式渲染失败:', e);
  361. }
  362. });
  363. }
  364. })
  365. .catch(error => {
  366. container.innerHTML = `
  367. <div class="apple-card p-12 text-center">
  368. <div class="text-red-400 mb-4">
  369. <i class="ri-error-warning-line text-6xl"></i>
  370. </div>
  371. <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
  372. <p class="text-sm text-red-500">${error.message || '未知错误'}</p>
  373. </div>
  374. `;
  375. });
  376. }
  377. // 分页加载函数(完全按照题目管理页面的格式)
  378. function loadPendingQuestionsByKpPage(kpCode, page) {
  379. // 更新URL参数,但不刷新页面
  380. const url = new URL(window.location);
  381. url.searchParams.set('kp_code', kpCode);
  382. if (page > 1) {
  383. url.searchParams.set('page', page);
  384. } else {
  385. url.searchParams.delete('page');
  386. }
  387. window.history.pushState({}, '', url);
  388. // 重新加载题目列表
  389. const linkElement = document.querySelector(`.kp-link[data-kp-code="${kpCode}"]`);
  390. const kpName = linkElement ? (linkElement.getAttribute('data-kp-name') || kpCode) : kpCode;
  391. loadPendingQuestionsByKp(kpCode, kpName, linkElement, page);
  392. }
  393. // 加载所有题目(根据筛选条件)
  394. function loadAllQuestionsByFilter(page = 1) {
  395. const container = document.getElementById('questions-container');
  396. if (!container) return;
  397. // 获取筛选参数
  398. const createByFilter = document.getElementById('filter-create-by')?.value || '';
  399. const createdAtFilter = document.getElementById('filter-created-at')?.value || '';
  400. // 如果没有筛选条件,不加载
  401. if (!createByFilter && !createdAtFilter) {
  402. // 如果有选择的知识点,加载该知识点;否则显示提示
  403. if (currentKpCode !== null) {
  404. const linkElement = document.querySelector(`.kp-link[data-kp-code="${currentKpCode}"]`);
  405. const kpName = linkElement ? (linkElement.getAttribute('data-kp-name') || currentKpCode) : currentKpCode;
  406. loadPendingQuestionsByKp(currentKpCode, kpName, linkElement, page);
  407. } else {
  408. container.innerHTML = `
  409. <div class="apple-card p-12 text-center">
  410. <div class="text-gray-400 mb-4">
  411. <i class="ri-file-check-line text-6xl"></i>
  412. </div>
  413. <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点或设置筛选条件</h3>
  414. <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的未审核题目,或使用筛选条件查看所有题目</p>
  415. </div>
  416. `;
  417. }
  418. return;
  419. }
  420. // 清除左侧选中状态
  421. document.querySelectorAll('.kp-link').forEach(link => {
  422. link.classList.remove('bg-orange-50', 'border-orange-400');
  423. });
  424. // 显示加载状态
  425. container.innerHTML = `
  426. <div class="apple-card p-12 text-center">
  427. <div class="text-orange-500 mb-4">
  428. <i class="ri-loader-4-line text-6xl animate-spin"></i>
  429. </div>
  430. <p class="text-gray-600">正在加载未审核题目...</p>
  431. </div>
  432. `;
  433. // 构建请求URL(使用'all'表示所有题目)
  434. let apiUrl = `/api/pending_questions_by_kp/all`;
  435. const params = new URLSearchParams();
  436. if (page > 1) {
  437. params.append('page', page);
  438. }
  439. if (createByFilter) {
  440. params.append('create_by', createByFilter);
  441. }
  442. if (createdAtFilter) {
  443. params.append('created_at', createdAtFilter);
  444. }
  445. if (params.toString()) {
  446. apiUrl += '?' + params.toString();
  447. }
  448. // 请求题目列表
  449. fetch(apiUrl)
  450. .then(response => response.json())
  451. .then(data => {
  452. if (!data.success) {
  453. throw new Error(data.error || '加载失败');
  454. }
  455. const questions = data.questions || [];
  456. const kpName = data.kp_name || '全部题目';
  457. // 获取分页信息
  458. const pagination = data.pagination || {};
  459. const currentPage = pagination.page || 1;
  460. const totalPages = pagination.total_pages || 1;
  461. const totalCount = pagination.total_count || questions.length;
  462. const pageSize = pagination.page_size || 20;
  463. const startIndex = (currentPage - 1) * pageSize;
  464. const endIndex = Math.min(startIndex + pageSize, totalCount);
  465. // 渲染题目列表(复用相同的渲染逻辑)
  466. let html = '';
  467. html += `<div class="space-y-3 mt-6">`;
  468. if (questions.length === 0) {
  469. html += `
  470. <div class="apple-card p-12 text-center">
  471. <div class="text-gray-400 mb-4">
  472. <i class="ri-file-list-line text-6xl"></i>
  473. </div>
  474. <h3 class="text-lg font-bold text-gray-600 mb-2">没有找到符合条件的题目</h3>
  475. <p class="text-sm text-gray-500">请尝试调整筛选条件</p>
  476. </div>
  477. `;
  478. } else {
  479. questions.forEach(q => {
  480. let auditBadge = '<span class="bg-orange-100 text-orange-700 px-2 py-0.5 rounded text-xs font-bold whitespace-nowrap">待审核</span>';
  481. let difficultyBadge = '';
  482. if (q.difficulty !== null && q.difficulty !== undefined) {
  483. const diff = parseFloat(q.difficulty);
  484. if (diff === 0.2 || Math.abs(diff - 0.2) < 0.1) {
  485. 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>';
  486. } else if (diff === 0.4 || Math.abs(diff - 0.4) < 0.1) {
  487. 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>';
  488. } else if (diff === 0.7 || Math.abs(diff - 0.7) < 0.1) {
  489. 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>';
  490. }
  491. }
  492. const typeMap = {'choice': '选择题', 'fill': '填空题', 'answer': '解答题'};
  493. const questionTypeText = typeMap[q.question_type] || q.question_type || '未分类';
  494. let gradeBadge = '';
  495. if (q.grade !== null && q.grade !== undefined) {
  496. const grade = parseInt(q.grade);
  497. if (grade === 1) {
  498. 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>';
  499. } else if (grade === 2) {
  500. 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>';
  501. } else if (grade === 3) {
  502. 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>';
  503. }
  504. }
  505. const stemText = (q.stem || '').replace(/<[^>]*>/g, '').substring(0, 150);
  506. html += `
  507. <a href="/detail/${q.question_code}"
  508. class="apple-card p-4 block group hover:shadow-lg transition-all border-l-4 border-transparent hover:border-blue-500">
  509. <div class="flex items-center gap-4">
  510. <div class="flex-shrink-0">
  511. <span class="text-xs font-mono text-gray-500 bg-gray-100 px-3 py-1.5 rounded font-semibold">${q.question_code}</span>
  512. </div>
  513. <div class="flex-1 min-w-0">
  514. <div class="text-sm text-gray-800 group-hover:text-blue-600 transition-colors line-clamp-1">
  515. ${stemText}${stemText.length >= 150 ? '...' : ''}
  516. </div>
  517. </div>
  518. <div class="flex items-center gap-3 flex-shrink-0">
  519. <span class="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">${questionTypeText}</span>
  520. ${gradeBadge}
  521. ${difficultyBadge}
  522. ${auditBadge}
  523. <span class="text-xs text-blue-600 font-medium opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">查看详情 →</span>
  524. </div>
  525. </div>
  526. </a>
  527. `;
  528. });
  529. // 添加分页控件
  530. if (totalPages > 1) {
  531. html += `
  532. <div class="mt-6 flex items-center justify-between">
  533. <div class="text-sm text-gray-600">
  534. 显示第 ${startIndex + 1}-${endIndex} 题,共 ${totalCount} 题
  535. </div>
  536. <div class="flex items-center gap-2">
  537. <button
  538. onclick="loadAllQuestionsByFilterPage(${currentPage - 1})"
  539. ${currentPage === 1 ? 'disabled' : ''}
  540. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  541. currentPage === 1
  542. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  543. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  544. }">
  545. <i class="ri-arrow-left-s-line"></i> 上一页
  546. </button>
  547. <div class="flex items-center gap-1">
  548. ${Array.from({length: totalPages}, (_, i) => i + 1).map(p => {
  549. if (p === 1 || p === totalPages || (p >= currentPage - 2 && p <= currentPage + 2)) {
  550. return `
  551. <button
  552. onclick="loadAllQuestionsByFilterPage(${p})"
  553. class="px-3 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  554. p === currentPage
  555. ? 'border-blue-500 bg-blue-500 text-white'
  556. : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
  557. }">
  558. ${p}
  559. </button>
  560. `;
  561. } else if (p === currentPage - 3 || p === currentPage + 3) {
  562. return '<span class="px-2 text-gray-400">...</span>';
  563. }
  564. return '';
  565. }).join('')}
  566. </div>
  567. <button
  568. onclick="loadAllQuestionsByFilterPage(${currentPage + 1})"
  569. ${currentPage === totalPages ? 'disabled' : ''}
  570. class="px-4 py-2 rounded-lg text-sm font-medium transition-all border-2 ${
  571. currentPage === totalPages
  572. ? 'border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed'
  573. : 'border-blue-300 bg-white text-blue-700 hover:bg-blue-50'
  574. }">
  575. 下一页 <i class="ri-arrow-right-s-line"></i>
  576. </button>
  577. </div>
  578. </div>
  579. `;
  580. }
  581. }
  582. html += '</div>';
  583. container.innerHTML = html;
  584. // 渲染数学公式
  585. if (window.renderMathInElement) {
  586. container.querySelectorAll('.math-render').forEach(el => {
  587. try {
  588. window.renderMathInElement(el, {
  589. delimiters: [
  590. {left: "$$", right: "$$", display: true},
  591. {left: "$", right: "$", display: false},
  592. {left: "\\(", right: "\\)", display: false},
  593. {left: "\\[", right: "\\]", display: true}
  594. ],
  595. throwOnError: false
  596. });
  597. } catch (e) {
  598. console.warn('数学公式渲染失败:', e);
  599. }
  600. });
  601. }
  602. })
  603. .catch(error => {
  604. container.innerHTML = `
  605. <div class="apple-card p-12 text-center">
  606. <div class="text-red-400 mb-4">
  607. <i class="ri-error-warning-line text-6xl"></i>
  608. </div>
  609. <h3 class="text-lg font-bold text-gray-600 mb-2">加载失败</h3>
  610. <p class="text-sm text-red-500">${error.message || '未知错误'}</p>
  611. </div>
  612. `;
  613. });
  614. }
  615. // 分页加载所有题目(根据筛选条件)
  616. function loadAllQuestionsByFilterPage(page) {
  617. loadAllQuestionsByFilter(page);
  618. }
  619. // 重置筛选条件
  620. function resetFilters() {
  621. document.getElementById('filter-create-by').value = '';
  622. const dateInput = document.getElementById('filter-created-at');
  623. if (dateInput) {
  624. dateInput.value = '';
  625. }
  626. // 重新加载当前知识点的题目,如果没有选择知识点则显示提示
  627. if (currentKpCode !== null) {
  628. const linkElement = document.querySelector(`.kp-link[data-kp-code="${currentKpCode}"]`);
  629. const kpName = linkElement ? (linkElement.getAttribute('data-kp-name') || currentKpCode) : currentKpCode;
  630. loadPendingQuestionsByKp(currentKpCode, kpName, linkElement, 1);
  631. } else {
  632. const container = document.getElementById('questions-container');
  633. if (container) {
  634. container.innerHTML = `
  635. <div class="apple-card p-12 text-center">
  636. <div class="text-gray-400 mb-4">
  637. <i class="ri-file-check-line text-6xl"></i>
  638. </div>
  639. <h3 class="text-lg font-bold text-gray-600 mb-2">请选择左侧知识点或设置筛选条件</h3>
  640. <p class="text-sm text-gray-500 mb-6">点击知识点查看该知识点下的未审核题目,或使用筛选条件查看所有题目</p>
  641. </div>
  642. `;
  643. }
  644. }
  645. }
  646. // 监听筛选条件变化
  647. document.addEventListener('DOMContentLoaded', function() {
  648. const createByFilter = document.getElementById('filter-create-by');
  649. const createdAtFilter = document.getElementById('filter-created-at');
  650. // 检查是否有筛选条件的函数
  651. function checkAndLoad() {
  652. const createBy = createByFilter?.value || '';
  653. const createdAt = createdAtFilter?.value || '';
  654. // 如果有筛选条件,加载所有题目
  655. if (createBy || createdAt) {
  656. loadAllQuestionsByFilter(1);
  657. } else {
  658. // 如果没有筛选条件,加载当前知识点的题目
  659. if (currentKpCode !== null) {
  660. const linkElement = document.querySelector(`.kp-link[data-kp-code="${currentKpCode}"]`);
  661. const kpName = linkElement ? (linkElement.getAttribute('data-kp-name') || currentKpCode) : currentKpCode;
  662. loadPendingQuestionsByKp(currentKpCode, kpName, linkElement, 1);
  663. }
  664. }
  665. }
  666. if (createByFilter) {
  667. createByFilter.addEventListener('change', checkAndLoad);
  668. }
  669. if (createdAtFilter) {
  670. // 日期选择器支持 change 和 input 事件
  671. createdAtFilter.addEventListener('change', checkAndLoad);
  672. createdAtFilter.addEventListener('input', checkAndLoad);
  673. }
  674. });
  675. // 页面加载完成后,初始化知识点目录
  676. document.addEventListener('DOMContentLoaded', function() {
  677. // 默认展开第一级节点
  678. const firstLevelNodes = document.querySelectorAll('.kp-node-item[data-level="0"]');
  679. firstLevelNodes.forEach(node => {
  680. const kpCode = node.getAttribute('data-kp-code');
  681. const expandBtn = node.querySelector('.kp-expand-btn');
  682. if (expandBtn && expandBtn.getAttribute('data-expanded') === 'true') {
  683. const childrenContainer = document.getElementById(`kp-children-${kpCode}`);
  684. if (childrenContainer) {
  685. childrenContainer.classList.remove('hidden');
  686. }
  687. }
  688. });
  689. });
  690. </script>
  691. {% endblock %}