kp_management.html 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. {% extends "layout.html" %}
  2. {% block page_title %}知识点管理{% endblock %}
  3. {% block content %}
  4. <!-- 树状节点宏定义 -->
  5. {% macro render_tree_node(kp, level) %}
  6. <div
  7. class="kp-node relative"
  8. data-kp-id="{{ kp.id }}"
  9. data-kp-code="{{ kp.kp_code }}"
  10. data-parent="{{ kp.parent_kp_code or '' }}"
  11. data-level="{{ level }}"
  12. data-grade="{{ kp.grade or '' }}">
  13. <!-- 节点卡片 -->
  14. <div class="flex items-start gap-4 group">
  15. <!-- 展开/折叠按钮区域 -->
  16. <div class="flex items-center gap-2 pt-4 flex-shrink-0">
  17. {% if kp.children|length > 0 %}
  18. <button
  19. onclick="toggleTreeNode('{{ kp.kp_code }}'); event.stopPropagation();"
  20. class="w-8 h-8 rounded-full bg-white border-2 border-gray-300 hover:border-blue-500 flex items-center justify-center text-gray-600 hover:text-blue-600 transition-all expand-btn z-10 shadow-sm hover:shadow-md"
  21. data-expanded="{% if level == 0 %}true{% else %}false{% endif %}">
  22. {% if level == 0 %}
  23. <i class="ri-subtract-line text-sm"></i>
  24. {% else %}
  25. <i class="ri-add-line text-sm"></i>
  26. {% endif %}
  27. </button>
  28. {% else %}
  29. <div class="w-8 h-8 flex items-center justify-center">
  30. <div class="w-2 h-2 rounded-full bg-gray-400"></div>
  31. </div>
  32. {% endif %}
  33. </div>
  34. <!-- 节点内容卡片 -->
  35. <div class="flex-1 apple-card p-5 hover:shadow-xl transition-all duration-300 group-hover:border-blue-300 border-2 border-transparent rounded-xl {% if kp.children|length > 0 %}cursor-pointer{% endif %}" {% if kp.children|length > 0 %}onclick="handleCardClick(event, '{{ kp.kp_code }}')"{% endif %}>
  36. <div class="flex items-start justify-between">
  37. <div class="flex-1 min-w-0">
  38. <div class="flex items-center gap-3 mb-3 flex-wrap">
  39. <span class="px-3 py-1.5 rounded-lg text-xs font-bold font-mono bg-gradient-to-r {% if level == 0 %}from-blue-500 to-indigo-600{% elif level == 1 %}from-green-500 to-emerald-600{% else %}from-orange-500 to-amber-600{% endif %} text-white shadow-md">
  40. {{ kp.kp_code }}
  41. </span>
  42. <h3 class="text-lg font-bold text-gray-800 group-hover:text-blue-600 transition-colors">
  43. {{ kp.name }}
  44. </h3>
  45. </div>
  46. <div class="flex items-center gap-4 text-sm text-gray-500 mt-2 flex-wrap">
  47. {% if kp.grade %}
  48. <span class="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-md">
  49. <i class="ri-graduation-cap-line text-green-500"></i>
  50. <span>{{ kp.grade }}</span>
  51. </span>
  52. {% endif %}
  53. {% if kp.question_count is defined and kp.question_count > 0 %}
  54. <span class="flex items-center gap-1.5 px-2 py-1 bg-gradient-to-r from-red-50 to-orange-50 border border-red-200 rounded-md text-red-600 shadow-sm">
  55. <i class="ri-file-list-line text-red-500"></i>
  56. <span class="font-bold">{{ kp.question_count }} 道题目</span>
  57. </span>
  58. {% endif %}
  59. {% if kp.has_children %}
  60. <span class="flex items-center gap-1.5 px-2 py-1 bg-blue-50 rounded-md text-blue-600">
  61. <i class="ri-node-tree"></i>
  62. <span class="font-semibold">{{ kp.children|length }} 个子节点</span>
  63. </span>
  64. {% endif %}
  65. </div>
  66. </div>
  67. <!-- 操作按钮 -->
  68. <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity ml-4 flex-shrink-0" onclick="event.stopPropagation()">
  69. <button
  70. onclick="showAddChildModal('{{ kp.kp_code }}', '{{ kp.name }}')"
  71. class="px-3 py-1.5 text-xs font-semibold text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 rounded-lg transition-all shadow-sm hover:shadow-md flex items-center gap-1.5"
  72. title="添加子知识点">
  73. <i class="ri-add-line"></i>
  74. <span>添加子节点</span>
  75. </button>
  76. <button
  77. onclick="showEditModal({{ kp.id }})"
  78. class="w-9 h-9 rounded-lg text-blue-600 bg-blue-50 hover:bg-blue-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
  79. title="编辑">
  80. <i class="ri-edit-line"></i>
  81. </button>
  82. <button
  83. onclick="showDeleteConfirm({{ kp.id }}, '{{ kp.kp_code }}', '{{ kp.name }}')"
  84. class="w-9 h-9 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 flex items-center justify-center transition-colors shadow-sm hover:shadow-md"
  85. title="删除">
  86. <i class="ri-delete-bin-line"></i>
  87. </button>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. <!-- 子节点容器 -->
  93. {% if kp.children|length > 0 %}
  94. <div class="children-container mt-3 ml-12 {% if level >= 1 %}hidden{% endif %}" id="children-{{ kp.kp_code }}" data-level="{{ level }}" data-parent-code="{{ kp.kp_code }}">
  95. {% for child in kp.children %}
  96. {{ render_tree_node(child, level + 1) }}
  97. {% endfor %}
  98. </div>
  99. {% endif %}
  100. </div>
  101. {% endmacro %}
  102. <div class="space-y-6">
  103. <!-- 学段统计卡片(只显示初中) -->
  104. <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  105. {% for grade_name, info in grade_info.items() %}
  106. {# 只显示初中,小学和高中已注释 #}
  107. <div
  108. onclick="filterByGrade('{{ grade_name }}')"
  109. data-grade="{{ grade_name }}"
  110. class="grade-card apple-card p-6 cursor-pointer transform transition-all duration-300 hover:scale-[1.02] hover:shadow-xl group {{ info.bg }} border-2 border-transparent hover:border-gray-300">
  111. <div class="flex items-center justify-between mb-4">
  112. <div class="w-16 h-16 rounded-2xl bg-gradient-to-br {{ info.color }} flex items-center justify-center text-white text-2xl shadow-lg group-hover:scale-110 transition-transform group-hover:shadow-xl">
  113. <i class="{{ info.icon }}"></i>
  114. </div>
  115. <div class="text-right">
  116. <div class="text-3xl font-bold text-gray-800 group-hover:text-gray-900 transition-colors">{{ info.count }}</div>
  117. <div class="text-sm text-gray-500 mt-1 font-medium">知识点</div>
  118. </div>
  119. </div>
  120. <div class="flex items-center justify-between mb-3">
  121. <h3 class="text-xl font-bold text-gray-800 group-hover:text-gray-900 transition-colors">{{ grade_name }}</h3>
  122. <div class="w-8 h-8 rounded-full bg-white/60 flex items-center justify-center group-hover:bg-white/80 transition-colors">
  123. <i class="ri-arrow-right-line text-gray-600 group-hover:text-gray-800 transition-colors"></i>
  124. </div>
  125. </div>
  126. <div class="mt-4 h-1.5 bg-white/60 rounded-full overflow-hidden shadow-inner">
  127. {% set total_count = grade_info.values()|sum(attribute='count') %}
  128. {% if total_count > 0 %}
  129. {% set percentage = (info.count / total_count * 100)|round(1) %}
  130. {% else %}
  131. {% set percentage = 0 %}
  132. {% endif %}
  133. <div class="h-full bg-gradient-to-r {{ info.color }} rounded-full transition-all duration-500 shadow-sm"
  134. style="width: {{ percentage }}%"></div>
  135. </div>
  136. </div>
  137. {% endfor %}
  138. </div>
  139. <!-- 页面标题和操作按钮 -->
  140. <div class="flex items-center justify-between">
  141. <!-- 搜索框 -->
  142. <div class="flex-1 max-w-md">
  143. <div class="relative">
  144. <input
  145. type="text"
  146. id="kpSearchInput"
  147. placeholder="搜索知识点名称或代码..."
  148. class="w-full px-4 py-2 pl-10 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
  149. onkeydown="handleSearchKeydown(event)"
  150. oninput="handleSearchInput(event)">
  151. <i class="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
  152. <button
  153. id="clearSearchBtn"
  154. onclick="clearSearch()"
  155. class="hidden absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors">
  156. <i class="ri-close-line"></i>
  157. </button>
  158. </div>
  159. </div>
  160. <div class="flex items-center gap-3">
  161. <button
  162. onclick="clearFilter()"
  163. id="clearFilterBtn"
  164. class="hidden px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors flex items-center gap-2">
  165. <i class="ri-close-line"></i>
  166. <span>清除筛选</span>
  167. </button>
  168. <button
  169. onclick="showAddModal()"
  170. class="btn-apple bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2">
  171. <i class="ri-add-circle-line"></i>
  172. <span>添加知识点</span>
  173. </button>
  174. </div>
  175. </div>
  176. <!-- 知识点树状结构 -->
  177. <div class="space-y-3" id="kpTreeContainer">
  178. {% for root_kp in kp_tree %}
  179. {{ render_tree_node(root_kp, 0) }}
  180. {% endfor %}
  181. </div>
  182. </div>
  183. <!-- 添加/编辑知识点模态框 -->
  184. <div id="kpModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
  185. <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
  186. <div class="p-6 border-b border-gray-200">
  187. <h2 id="modalTitle" class="text-xl font-bold text-gray-800">添加知识点</h2>
  188. </div>
  189. <form id="kpForm" class="p-6 space-y-4">
  190. <input type="hidden" id="kpId" name="id">
  191. <div>
  192. <label class="block text-sm font-semibold text-gray-700 mb-2">知识点代码 *</label>
  193. <input
  194. type="text"
  195. id="kpCode"
  196. name="kp_code"
  197. required
  198. class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
  199. placeholder="例如:M01">
  200. </div>
  201. <div>
  202. <label class="block text-sm font-semibold text-gray-700 mb-2">名称 *</label>
  203. <input
  204. type="text"
  205. id="kpName"
  206. name="name"
  207. required
  208. class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
  209. placeholder="请输入知识点名称">
  210. </div>
  211. <div>
  212. <label class="block text-sm font-semibold text-gray-700 mb-2">年级</label>
  213. <input
  214. type="text"
  215. id="kpGrade"
  216. name="grade"
  217. class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
  218. placeholder="例如:初中">
  219. </div>
  220. <!-- 科目字段隐藏,默认传"数学" -->
  221. <input type="hidden" id="kpSubject" name="subject" value="数学">
  222. <div>
  223. <label class="block text-sm font-semibold text-gray-700 mb-2">父知识点</label>
  224. <select
  225. id="kpParent"
  226. name="parent_kp_code"
  227. class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
  228. <option value="">无(顶级知识点)</option>
  229. {% for option in kp_options %}
  230. <option value="{{ option.kp_code }}">{{ option.kp_code }} - {{ option.name }}</option>
  231. {% endfor %}
  232. </select>
  233. </div>
  234. <div class="flex justify-end gap-3 pt-4 border-t border-gray-200">
  235. <button
  236. type="button"
  237. onclick="closeModal()"
  238. class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
  239. 取消
  240. </button>
  241. <button
  242. type="submit"
  243. class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 rounded-lg transition-colors">
  244. 保存
  245. </button>
  246. </div>
  247. </form>
  248. </div>
  249. </div>
  250. <script>
  251. // 知识点选项数据
  252. const kpOptions = {{ kp_options|tojson|safe }};
  253. // 显示添加模态框
  254. function showAddModal() {
  255. document.getElementById('modalTitle').textContent = '添加知识点';
  256. document.getElementById('kpForm').reset();
  257. document.getElementById('kpId').value = '';
  258. document.getElementById('kpCode').disabled = false;
  259. document.getElementById('kpSubject').value = '数学'; // 默认科目为数学
  260. document.getElementById('kpModal').classList.remove('hidden');
  261. }
  262. // 显示编辑模态框
  263. async function showEditModal(kpId) {
  264. try {
  265. const response = await fetch(`/api/kp/get/${kpId}`);
  266. const result = await response.json();
  267. if (result.success) {
  268. const kp = result.data;
  269. document.getElementById('modalTitle').textContent = '编辑知识点';
  270. document.getElementById('kpId').value = kp.id;
  271. document.getElementById('kpCode').value = kp.kp_code;
  272. document.getElementById('kpCode').disabled = true; // 编辑时不允许修改代码
  273. document.getElementById('kpName').value = kp.name || '';
  274. document.getElementById('kpSubject').value = kp.subject || '数学'; // 如果为空则默认为数学
  275. document.getElementById('kpGrade').value = kp.grade || '';
  276. document.getElementById('kpParent').value = kp.parent_kp_code || '';
  277. document.getElementById('kpModal').classList.remove('hidden');
  278. } else {
  279. if (window.customAlert) {
  280. window.customAlert('获取知识点信息失败: ' + result.error);
  281. } else {
  282. alert('获取知识点信息失败: ' + result.error);
  283. }
  284. }
  285. } catch (error) {
  286. console.error('Error:', error);
  287. if (window.customAlert) {
  288. window.customAlert('获取知识点信息失败: ' + error.message);
  289. } else {
  290. alert('获取知识点信息失败: ' + error.message);
  291. }
  292. }
  293. }
  294. // 关闭模态框
  295. function closeModal() {
  296. document.getElementById('kpModal').classList.add('hidden');
  297. }
  298. // 表单提交
  299. document.getElementById('kpForm').addEventListener('submit', async function(e) {
  300. e.preventDefault();
  301. const kpId = document.getElementById('kpId').value;
  302. const formData = {
  303. kp_code: (document.getElementById('kpCode').value || '').trim(),
  304. name: (document.getElementById('kpName').value || '').trim(),
  305. subject: (document.getElementById('kpSubject').value || '').trim() || '数学', // 默认科目为数学
  306. grade: (document.getElementById('kpGrade').value || '').trim() || null,
  307. parent_kp_code: (document.getElementById('kpParent').value || '').trim() || null
  308. };
  309. try {
  310. let response;
  311. if (kpId) {
  312. // 更新
  313. response = await fetch(`/api/kp/update/${kpId}`, {
  314. method: 'POST',
  315. headers: {'Content-Type': 'application/json'},
  316. body: JSON.stringify(formData)
  317. });
  318. } else {
  319. // 创建
  320. response = await fetch('/api/kp/create', {
  321. method: 'POST',
  322. headers: {'Content-Type': 'application/json'},
  323. body: JSON.stringify(formData)
  324. });
  325. }
  326. const result = await response.json();
  327. if (result.success) {
  328. if (window.customAlert) {
  329. window.customAlert(result.message || '操作成功', () => {
  330. window.location.reload();
  331. });
  332. } else {
  333. alert(result.message || '操作成功');
  334. window.location.reload();
  335. }
  336. } else {
  337. if (window.customAlert) {
  338. window.customAlert('操作失败: ' + result.error);
  339. } else {
  340. alert('操作失败: ' + result.error);
  341. }
  342. }
  343. } catch (error) {
  344. console.error('Error:', error);
  345. if (window.customAlert) {
  346. window.customAlert('操作失败: ' + error.message);
  347. } else {
  348. alert('操作失败: ' + error.message);
  349. }
  350. }
  351. });
  352. // 切换树节点展开/折叠
  353. function toggleTreeNode(kpCode) {
  354. const childrenContainer = document.getElementById(`children-${kpCode}`);
  355. if (!childrenContainer) return;
  356. // 找到对应的展开按钮(通过查找包含该按钮的节点)
  357. const kpNode = childrenContainer.closest('.kp-node');
  358. if (!kpNode) return;
  359. const expandBtn = kpNode.querySelector('.expand-btn');
  360. if (!expandBtn) return;
  361. const isExpanded = expandBtn.getAttribute('data-expanded') === 'true';
  362. if (isExpanded) {
  363. // 折叠:隐藏所有子节点
  364. childrenContainer.classList.add('hidden');
  365. expandBtn.setAttribute('data-expanded', 'false');
  366. const icon = expandBtn.querySelector('i');
  367. if (icon) {
  368. icon.className = 'ri-add-line text-sm';
  369. }
  370. } else {
  371. // 展开:显示直接子节点
  372. childrenContainer.classList.remove('hidden');
  373. expandBtn.setAttribute('data-expanded', 'true');
  374. const icon = expandBtn.querySelector('i');
  375. if (icon) {
  376. icon.className = 'ri-subtract-line text-sm';
  377. }
  378. }
  379. }
  380. // 强制展开节点(用于搜索时展开父节点)
  381. function expandTreeNode(kpCode) {
  382. const childrenContainer = document.getElementById(`children-${kpCode}`);
  383. if (!childrenContainer) return;
  384. const kpNode = childrenContainer.closest('.kp-node');
  385. if (!kpNode) return;
  386. const expandBtn = kpNode.querySelector('.expand-btn');
  387. if (!expandBtn) return;
  388. // 展开节点
  389. childrenContainer.classList.remove('hidden');
  390. expandBtn.setAttribute('data-expanded', 'true');
  391. const icon = expandBtn.querySelector('i');
  392. if (icon) {
  393. icon.className = 'ri-subtract-line text-sm';
  394. }
  395. }
  396. // 展开所有父节点直到根节点
  397. function expandAllParents(kpCode) {
  398. const kpNode = document.querySelector(`.kp-node[data-kp-code="${kpCode}"]`);
  399. if (!kpNode) return;
  400. let currentParentCode = kpNode.getAttribute('data-parent');
  401. // 递归展开所有父节点
  402. while (currentParentCode && currentParentCode !== '') {
  403. expandTreeNode(currentParentCode);
  404. // 获取父节点的父节点
  405. const parentNode = document.querySelector(`.kp-node[data-kp-code="${currentParentCode}"]`);
  406. if (parentNode) {
  407. currentParentCode = parentNode.getAttribute('data-parent');
  408. } else {
  409. break;
  410. }
  411. }
  412. }
  413. // 搜索知识点
  414. function searchKnowledgePoint(searchText) {
  415. if (!searchText || searchText.trim() === '') {
  416. clearSearch();
  417. return;
  418. }
  419. const searchLower = searchText.toLowerCase().trim();
  420. const allKpNodes = document.querySelectorAll('.kp-node');
  421. let foundNodes = [];
  422. // 清除之前的高亮
  423. allKpNodes.forEach(node => {
  424. const card = node.querySelector('.apple-card');
  425. if (card) {
  426. card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
  427. }
  428. });
  429. // 搜索匹配的知识点(收集所有匹配的节点)
  430. for (const node of allKpNodes) {
  431. const kpCode = node.getAttribute('data-kp-code') || '';
  432. const kpName = node.querySelector('h3')?.textContent || '';
  433. // 模糊匹配:代码或名称包含搜索文本
  434. if (kpCode.toLowerCase().includes(searchLower) || kpName.toLowerCase().includes(searchLower)) {
  435. foundNodes.push({ node, kpCode, kpName });
  436. }
  437. }
  438. if (foundNodes.length === 0) {
  439. // 如果没有找到,显示提示
  440. if (window.customAlert) {
  441. window.customAlert('未找到匹配的知识点');
  442. } else {
  443. alert('未找到匹配的知识点');
  444. }
  445. return;
  446. }
  447. // 优先匹配代码,其次匹配名称
  448. foundNodes.sort((a, b) => {
  449. const aCodeMatch = a.kpCode.toLowerCase().includes(searchLower);
  450. const bCodeMatch = b.kpCode.toLowerCase().includes(searchLower);
  451. if (aCodeMatch && !bCodeMatch) return -1;
  452. if (!aCodeMatch && bCodeMatch) return 1;
  453. return 0;
  454. });
  455. // 跳转到第一个匹配的知识点
  456. const firstMatch = foundNodes[0];
  457. const targetNode = firstMatch.node;
  458. const targetKpCode = firstMatch.kpCode;
  459. // 展开所有父节点
  460. expandAllParents(targetKpCode);
  461. // 高亮当前节点
  462. const card = targetNode.querySelector('.apple-card');
  463. if (card) {
  464. card.classList.add('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
  465. }
  466. // 滚动到该节点(等待展开动画完成)
  467. setTimeout(() => {
  468. targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
  469. // 3秒后移除高亮
  470. setTimeout(() => {
  471. if (card) {
  472. card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2', 'bg-blue-50');
  473. }
  474. }, 3000);
  475. }, 200);
  476. // 如果有多个匹配,在控制台提示
  477. if (foundNodes.length > 1) {
  478. console.log(`找到 ${foundNodes.length} 个匹配的知识点,已跳转到第一个`);
  479. }
  480. }
  481. // 处理搜索输入(实时搜索)
  482. let searchTimeout = null;
  483. function handleSearchInput(event) {
  484. const searchText = event.target.value;
  485. const clearBtn = document.getElementById('clearSearchBtn');
  486. if (searchText.trim() !== '') {
  487. clearBtn.classList.remove('hidden');
  488. // 防抖:延迟500ms后执行搜索
  489. clearTimeout(searchTimeout);
  490. searchTimeout = setTimeout(() => {
  491. searchKnowledgePoint(searchText);
  492. }, 500);
  493. } else {
  494. clearBtn.classList.add('hidden');
  495. clearSearch();
  496. }
  497. }
  498. // 处理搜索框回车键(立即搜索)
  499. function handleSearchKeydown(event) {
  500. if (event.key === 'Enter') {
  501. event.preventDefault();
  502. clearTimeout(searchTimeout);
  503. const searchText = event.target.value;
  504. searchKnowledgePoint(searchText);
  505. }
  506. }
  507. // 清除搜索
  508. function clearSearch() {
  509. const searchInput = document.getElementById('kpSearchInput');
  510. const clearBtn = document.getElementById('clearSearchBtn');
  511. searchInput.value = '';
  512. clearBtn.classList.add('hidden');
  513. // 清除高亮
  514. const allKpNodes = document.querySelectorAll('.kp-node');
  515. allKpNodes.forEach(node => {
  516. const card = node.querySelector('.apple-card');
  517. if (card) {
  518. card.classList.remove('ring-4', 'ring-blue-500', 'ring-offset-2');
  519. }
  520. });
  521. }
  522. // 处理卡片点击事件(点击空白区域展开/折叠)
  523. function handleCardClick(event, kpCode) {
  524. // 如果点击的是按钮或链接,不触发展开/折叠
  525. if (event.target.closest('button') || event.target.closest('a')) {
  526. return;
  527. }
  528. // 触发展开/折叠
  529. toggleTreeNode(kpCode);
  530. }
  531. // 显示添加子节点模态框
  532. function showAddChildModal(parentKpCode, parentKpName) {
  533. document.getElementById('modalTitle').textContent = `添加子知识点(父节点:${parentKpCode} - ${parentKpName})`;
  534. document.getElementById('kpId').value = '';
  535. document.getElementById('kpCode').value = '';
  536. document.getElementById('kpName').value = '';
  537. document.getElementById('kpSubject').value = '数学'; // 默认科目为数学
  538. document.getElementById('kpGrade').value = '';
  539. document.getElementById('kpParent').value = parentKpCode;
  540. document.getElementById('kpCode').disabled = false;
  541. document.getElementById('kpModal').classList.remove('hidden');
  542. }
  543. // 删除确认
  544. function showDeleteConfirm(kpId, kpCode, kpName) {
  545. if (confirm(`确定要删除知识点 "${kpCode} - ${kpName}" 吗?\n\n注意:如果该知识点下有子知识点,将无法删除。`)) {
  546. deleteKp(kpId);
  547. }
  548. }
  549. // 删除知识点
  550. async function deleteKp(kpId) {
  551. try {
  552. const response = await fetch(`/api/kp/delete/${kpId}`, {
  553. method: 'POST',
  554. headers: {'Content-Type': 'application/json'}
  555. });
  556. const result = await response.json();
  557. if (result.success) {
  558. if (window.customAlert) {
  559. window.customAlert(result.message || '删除成功', () => {
  560. window.location.reload();
  561. });
  562. } else {
  563. alert(result.message || '删除成功');
  564. window.location.reload();
  565. }
  566. } else {
  567. if (window.customAlert) {
  568. window.customAlert('删除失败: ' + result.error);
  569. } else {
  570. alert('删除失败: ' + result.error);
  571. }
  572. }
  573. } catch (error) {
  574. console.error('Error:', error);
  575. if (window.customAlert) {
  576. window.customAlert('删除失败: ' + error.message);
  577. } else {
  578. alert('删除失败: ' + error.message);
  579. }
  580. }
  581. }
  582. // 点击模态框外部关闭
  583. document.getElementById('kpModal').addEventListener('click', function(e) {
  584. if (e.target === this) {
  585. closeModal();
  586. }
  587. });
  588. // 当前筛选的学段
  589. let currentFilterGrade = null;
  590. // 按学段筛选
  591. function filterByGrade(grade) {
  592. currentFilterGrade = grade;
  593. // 显示清除筛选按钮
  594. document.getElementById('clearFilterBtn').classList.remove('hidden');
  595. // 获取所有行
  596. const allRows = Array.from(document.querySelectorAll('.kp-row'));
  597. // 先隐藏所有行
  598. allRows.forEach(row => {
  599. row.classList.add('hidden');
  600. });
  601. // 显示匹配学段的行及其父节点
  602. allRows.forEach(row => {
  603. const rowGrade = row.getAttribute('data-grade');
  604. if (rowGrade === grade) {
  605. // 显示匹配的行
  606. row.classList.remove('hidden');
  607. // 确保父节点也显示
  608. showParentRows(row);
  609. }
  610. });
  611. // 重新计算可见行数
  612. const visibleCount = Array.from(document.querySelectorAll('.kp-row:not(.hidden)')).length;
  613. // 筛选信息已移除
  614. // 高亮选中的学段卡片
  615. document.querySelectorAll('.grade-card').forEach(card => {
  616. card.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
  617. });
  618. // 找到被点击的卡片并高亮
  619. const clickedCard = document.querySelector(`.grade-card[data-grade="${grade}"]`);
  620. if (clickedCard) {
  621. clickedCard.classList.add('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
  622. }
  623. }
  624. // 显示父节点
  625. function showParentRows(row) {
  626. const parentCode = row.getAttribute('data-parent');
  627. if (parentCode) {
  628. const parentRow = document.querySelector(`tr[data-kp-code="${parentCode}"]`);
  629. if (parentRow) {
  630. parentRow.classList.remove('hidden');
  631. showParentRows(parentRow);
  632. }
  633. }
  634. }
  635. // 清除筛选
  636. function clearFilter() {
  637. currentFilterGrade = null;
  638. document.getElementById('clearFilterBtn').classList.add('hidden');
  639. // 恢复默认显示状态(level 0 和 level 1)
  640. const allRows = Array.from(document.querySelectorAll('.kp-row'));
  641. allRows.forEach(row => {
  642. const level = parseInt(row.getAttribute('data-level'));
  643. if (level <= 1) {
  644. row.classList.remove('hidden');
  645. } else {
  646. row.classList.add('hidden');
  647. }
  648. });
  649. // 重置展开按钮状态
  650. document.querySelectorAll('.expand-btn').forEach(btn => {
  651. const row = btn.closest('tr');
  652. const level = parseInt(row.getAttribute('data-level'));
  653. if (level === 0) {
  654. btn.setAttribute('data-expanded', 'true');
  655. btn.innerHTML = '<i class="ri-arrow-down-s-line"></i>';
  656. } else {
  657. btn.setAttribute('data-expanded', 'false');
  658. btn.innerHTML = '<i class="ri-arrow-right-s-line"></i>';
  659. }
  660. });
  661. const totalCount = {{ knowledge_points|length }};
  662. // 筛选信息已移除
  663. // 移除卡片高亮
  664. document.querySelectorAll('.grade-card').forEach(card => {
  665. card.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-2', 'border-blue-400');
  666. });
  667. }
  668. // 展开/折叠子知识点
  669. function toggleChildren(kpCode) {
  670. const row = document.querySelector(`tr[data-kp-code="${kpCode}"]`);
  671. if (!row) return;
  672. const btn = row.querySelector('.expand-btn');
  673. const isExpanded = btn.getAttribute('data-expanded') === 'true';
  674. // 递归函数:切换所有子节点的显示状态
  675. function toggleChildrenRecursive(parentCode, shouldShow) {
  676. const allRows = Array.from(document.querySelectorAll('.kp-row'));
  677. const parentRow = allRows.find(r => r.getAttribute('data-kp-code') === parentCode);
  678. if (!parentRow) return;
  679. const parentLevel = parseInt(parentRow.getAttribute('data-level'));
  680. const parentIndex = allRows.indexOf(parentRow);
  681. // 找到所有直接子节点
  682. for (let i = parentIndex + 1; i < allRows.length; i++) {
  683. const childRow = allRows[i];
  684. const childLevel = parseInt(childRow.getAttribute('data-level'));
  685. const childParent = childRow.getAttribute('data-parent');
  686. // 如果是直接子节点
  687. if (childLevel === parentLevel + 1 && childParent === parentCode) {
  688. if (shouldShow) {
  689. childRow.classList.remove('hidden');
  690. // 如果子节点是展开的,继续展开它的子节点
  691. const childBtn = childRow.querySelector('.expand-btn');
  692. if (childBtn && childBtn.getAttribute('data-expanded') === 'true') {
  693. toggleChildrenRecursive(childRow.getAttribute('data-kp-code'), true);
  694. }
  695. } else {
  696. childRow.classList.add('hidden');
  697. // 递归隐藏所有子节点
  698. toggleChildrenRecursive(childRow.getAttribute('data-kp-code'), false);
  699. }
  700. } else if (childLevel <= parentLevel) {
  701. // 遇到同级或上级节点,停止
  702. break;
  703. }
  704. }
  705. }
  706. // 切换显示状态
  707. toggleChildrenRecursive(kpCode, !isExpanded);
  708. // 更新按钮状态
  709. if (isExpanded) {
  710. btn.setAttribute('data-expanded', 'false');
  711. btn.innerHTML = '<i class="ri-arrow-right-s-line"></i>';
  712. } else {
  713. btn.setAttribute('data-expanded', 'true');
  714. btn.innerHTML = '<i class="ri-arrow-down-s-line"></i>';
  715. }
  716. }
  717. </script>
  718. {% endblock %}