]> acesimba.cloud Git - studybase.git/commitdiff
update: 修复章节列表和知识点列表为空问题,并增加排序效果
authorroot <root@localhost.localdomain>
Tue, 19 May 2026 12:41:58 +0000 (20:41 +0800)
committerroot <root@localhost.localdomain>
Tue, 19 May 2026 12:41:58 +0000 (20:41 +0800)
app.py
templates/subject/chapters.html
templates/subject/knowledge.html

diff --git a/app.py b/app.py
index 8c80782d942f5b21c3b44f147fa68efe08485e62..959a0ffd647e73b27cb4700105351ecc615de923 100644 (file)
--- a/app.py
+++ b/app.py
@@ -3,13 +3,28 @@ from database import query_db
 
 app = Flask(__name__)
 
-# --- 1. 页面路由 ---
+# ==================== 1. 核心页面路由 ====================
 
 @app.route('/')
 def index():
     return render_template('index.html')
 
-# 科目首页
+# --- 题目列表模块 ---
+@app.route('/questions/')
+def questions_list():
+    return render_template('questions/index.html')
+
+# --- 试卷列表模块 ---
+@app.route('/papers/')
+def papers_list():
+    return render_template('papers/index.html')
+
+# --- 系统设定模块 ---
+@app.route('/settings/')
+def settings_page():
+    return render_template('settings/index.html')
+
+# --- 错题分析模块 (刚才修复的部分) ---
 @app.route('/subject/')
 def subject_portal():
     subjects = query_db("SELECT ID, Subject as name FROM Subject WHERE enabled = 1")
@@ -17,144 +32,103 @@ def subject_portal():
     default_id = subjects[0]['ID'] if subjects else None
     return render_template('subject/index.html', subjects=subjects, stages=stages, default_id=default_id)
 
-# 章节分析页
 @app.route('/subject/chapters')
 def subject_chapters():
     subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject")
     return render_template('subject/chapters.html', subjects=subjects)
 
-# 知识点分析页
 @app.route('/subject/knowledge')
 def subject_knowledge():
     subjects = query_db("SELECT DISTINCT Subject FROM Subject ORDER BY Subject")
     chapter_id = request.args.get('chapter_id', '')
     return render_template('subject/knowledge.html', subjects=subjects, chapter_id=chapter_id)
 
-@app.route('/questions/')
-def questions(): return render_template('questions/index.html')
-
-@app.route('/papers/')
-def papers(): return render_template('papers/index.html')
-
-@app.route('/settings/')
-def settings(): return render_template('settings/index.html')
 
+# ==================== 2. API 数据接口 ====================
 
-# --- 2. API 接口 ---
-
-# 科目概览统计 API
+# 科目概览统计
 @app.route('/api/subject_stats')
 def get_subject_stats():
     sub_id = request.args.get('subject_id')
-    stage_ids = request.args.get('stage_ids', '').split(',')
-    if not sub_id or not stage_ids or stage_ids == ['']:
-        return jsonify({"error": "参数错误"}), 400
+    stage_ids_raw = request.args.get('stage_ids', '')
+    stage_ids = [sid for sid in stage_ids_raw.split(',') if sid]
+    
+    if not sub_id or not stage_ids:
+        return jsonify({"wrong": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0},
+                        "push": {"total": 0, "fixed": 0, "ratio": "0%", "remained": 0}})
 
     placeholders = ','.join(['?'] * len(stage_ids))
-    
-    def get_count(q_type, extra=""):
-        sql = f"SELECT COUNT(*) as c FROM questions WHERE question_type=? AND subject_id=? AND stage_id IN ({placeholders}) {extra}"
-        res = query_db(sql, [q_type, sub_id] + stage_ids, one=True)
+    def get_count(q_type, extra_sql=""):
+        sql = f"SELECT COUNT(*) as c FROM questions WHERE subject_id = ? AND stage_id IN ({placeholders}) AND question_type = ? {extra_sql}"
+        res = query_db(sql, [sub_id] + stage_ids + [q_type], one=True)
         return res['c'] if res else 0
 
-    w_total = get_count('错题')
-    w_fixed = get_count('错题', 'AND right_times > 0')
-    p_total = get_count('推题')
-    p_fixed = get_count('推题', 'AND right_times > 0')
-
-    return jsonify({
-        "wrong": {"total": w_total, "fixed": w_fixed, "ratio": f"{(w_fixed/w_total*100):.1f}%" if w_total>0 else "0%", "remained": w_total-w_fixed},
-        "push": {"total": p_total, "fixed": p_fixed, "ratio": f"{(p_fixed/p_total*100):.1f}%" if p_total>0 else "0%", "remained": p_total-p_fixed}
-    })
+    try:
+        w_total = get_count('错题'); w_fixed = get_count('错题', 'AND right_times > 0')
+        p_total = get_count('推题'); p_fixed = get_count('推题', 'AND right_times > 0')
+        return jsonify({
+            "wrong": {"total": w_total, "fixed": w_fixed, "ratio": f"{(w_fixed/w_total*100):.1f}%" if w_total>0 else "0%", "remained": w_total-w_fixed},
+            "push": {"total": p_total, "fixed": p_fixed, "ratio": f"{(p_fixed/p_total*100):.1f}%" if p_total>0 else "0%", "remained": p_total-p_fixed}
+        })
+    except Exception as e:
+        return jsonify({"error": str(e)}), 500
 
-# 章节数据 API (基于顶级知识点)
+# 章节数据接口 (已适配 chapter_desc)
 @app.route('/api/chapters')
 def get_chapters():
-    subject = request.args.get('subject', '全部')
+    subject_name = request.args.get('subject', '全部')
     keyword = request.args.get('keyword', '')
     params = []
-    
-    # 过滤 parent_id 为空作为章节
-    where = " WHERE kp.parent_id IS NULL AND kp.is_enabled = 1"
-    if subject != '全部':
-        where += " AND s.Subject = ?"
-        params.append(subject)
+    where = " WHERE 1=1"
+    if subject_name != '全部':
+        where += " AND s.Subject = ?"; params.append(subject_name)
     if keyword:
-        where += " AND kp.content LIKE ?"
-        params.append(f"%{keyword}%")
+        where += " AND c.chapter_desc LIKE ?"; params.append(f"%{keyword}%")
 
     sql = f"""
-    SELECT 
-        kp.id AS chapter_id, 
-        s.Subject AS SubjectName, 
-        kp.content AS ChapterDesc,
-        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r WHERE r.kp_id = kp.id) AS total,
-        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r 
-         JOIN questions q ON r.question_id = q.id 
-         WHERE r.kp_id = kp.id AND q.right_times > 0) AS correct
-    FROM knowledge_point kp 
-    JOIN Subject s ON kp.subject_id = s.ID 
-    {where}
+    SELECT c.id AS chapter_id, s.Subject AS SubjectName, c.chapter_desc AS ChapterDesc,
+        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id WHERE kp.chapter_id = c.id) AS total,
+        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN knowledge_point kp ON r.kp_id = kp.id JOIN questions q ON r.question_id = q.id WHERE kp.chapter_id = c.id AND q.right_times > 0) AS correct
+    FROM chapters c JOIN Subject s ON c.subject_id = s.ID {where}
     """
     try:
         rows = query_db(sql, params)
-        result = []
-        for row in rows:
-            item = dict(row) # 必须转字典,Row 不支持赋值
+        result = [dict(row) for row in rows]
+        for item in result:
             item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0)
-            # 只要有题目就显示
-            if item['total'] > 0:
-                result.append(item)
-        return jsonify(result)
+        return jsonify([i for i in result if i['total'] > 0])
     except Exception as e:
-        print(f"SQL Error in chapters: {e}")
         return jsonify({"error": str(e)}), 500
 
-# 知识点数据 API
+# 知识点数据接口
 @app.route('/api/knowledge')
 def get_knowledge():
-    subject = request.args.get('subject', '全部')
+    subject_name = request.args.get('subject', '全部')
     keyword = request.args.get('keyword', '')
     chapter_id = request.args.get('chapter_id', '')
     params = []
-    
     where = " WHERE kp.is_enabled = 1"
     if chapter_id:
-        # 如果有章节ID,查询该节点及其子节点
-        where += " AND (kp.id = ? OR kp.parent_id = ?)"
-        params.extend([chapter_id, chapter_id])
-    if subject != '全部':
-        where += " AND s.Subject = ?"
-        params.append(subject)
+        where += " AND kp.chapter_id = ?"; params.append(chapter_id)
+    if subject_name != '全部':
+        where += " AND s.Subject = ?"; params.append(subject_name)
     if keyword:
-        where += " AND kp.content LIKE ?"
-        params.append(f"%{keyword}%")
+        where += " AND kp.content LIKE ?"; params.append(f"%{keyword}%")
 
     sql = f"""
-    SELECT 
-        kp.id, 
-        s.Subject as subject_name, 
-        kp.content,
+    SELECT kp.id, s.Subject as subject_name, kp.content,
         (SELECT COUNT(*) FROM question_kp_relation r WHERE r.kp_id = kp.id) as total,
-        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r 
-         JOIN questions q ON r.question_id = q.id 
-         WHERE r.kp_id = kp.id AND q.right_times >= 1) as correct
-    FROM knowledge_point kp 
-    JOIN Subject s ON kp.subject_id = s.ID 
-    {where}
+        (SELECT COUNT(DISTINCT r.question_id) FROM question_kp_relation r JOIN questions q ON r.question_id = q.id WHERE r.kp_id = kp.id AND q.right_times >= 1) as correct
+    FROM knowledge_point kp JOIN Subject s ON kp.subject_id = s.ID {where}
     """
     try:
         rows = query_db(sql, params)
-        result = []
-        for row in rows:
-            item = dict(row)
+        result = [dict(row) for row in rows]
+        for item in result:
             item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0)
-            result.append(item)
         return jsonify(result)
     except Exception as e:
-        print(f"SQL Error in knowledge: {e}")
         return jsonify({"error": str(e)}), 500
 
 if __name__ == '__main__':
-    # 确保在服务器上运行时监听 0.0.0.0
     app.run(host='0.0.0.0', port=5000, debug=True)
index b7d5cba349cb05898954c7d82dfcf378ed4d6c08..75a0ecaee56c6f5f955e7b0a87095c0290e6d5c6 100644 (file)
     <title>章节分析</title>
     <link rel="stylesheet" href="/static/css/style.css">
     <style>
-        .filter-bar { 
-            background: white; 
-            padding: 15px; 
-            margin-bottom: 20px; 
-            border-radius: 12px; 
-            display: flex; 
-            gap: 10px; 
-            flex-wrap: wrap;
-            box-shadow: 0 2px 8px rgba(0,0,0,0.05);
-        }
-        .filter-bar select, .filter-bar input {
-            padding: 8px;
-            border: 1px solid #ddd;
-            border-radius: 6px;
-            flex: 1;
-            min-width: 120px;
-        }
-        .filter-bar button {
-            background: #4A90E2;
-            color: white;
-            border: none;
-            padding: 8px 20px;
-            border-radius: 6px;
-            cursor: pointer;
-        }
-        .table-container {
-            background: white;
-            border-radius: 12px;
-            overflow-x: auto;
-            box-shadow: 0 2px 8px rgba(0,0,0,0.05);
-        }
-        table { 
-            width: 100%; 
-            border-collapse: collapse; 
-            min-width: 500px;
-        }
-        th, td { 
-            padding: 15px 12px; 
-            text-align: left; 
-            border-bottom: 1px solid #f0f0f0; 
-            font-size: 14px;
-        }
-        th { 
-            background: #f8f9fa; 
-            color: #666; 
-            font-weight: bold;
-        }
-        .clickable { 
-            color: #4A90E2; 
-            cursor: pointer; 
-            font-weight: 500;
-        }
-        .clickable:hover {
-            text-decoration: underline;
-        }
-        .num-badge { 
-            font-weight: bold; 
-            color: #d32f2f; 
-            background: #fff1f0;
-            padding: 2px 8px;
-            border-radius: 4px;
-        }
-        .total-badge {
-            color: #666;
-            background: #f5f5f5;
-            padding: 2px 8px;
-            border-radius: 4px;
-        }
-        .loading-tip {
-            text-align: center;
-            padding: 20px;
-            color: #999;
-        }
+        .filter-bar { background: white; padding: 12px; margin-bottom: 15px; border-radius: 12px; display: flex; gap: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); align-items: center; }
+        /* 缩短科目选择框 */
+        #subjectFilter { width: 90px; flex: none; padding: 8px 4px; border: 1px solid #ddd; border-radius: 6px; }
+        #keyword { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; min-width: 0; }
+        .btn-query { background: #4A90E2; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; white-space: nowrap; }
+        /* 新增:筛选栏中的返回按钮 */
+        .btn-back-nav { background: #f0f0f0; color: #666; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; white-space: nowrap; text-decoration: none; font-size: 14px; }
+        
+        .table-container { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 50px; }
+        table { width: 100%; border-collapse: collapse; }
+        th, td { padding: 12px 10px; text-align: left; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
+        th { background-color: #f8f9fa; color: #666; font-weight: 600; cursor: pointer; position: relative; }
+        th:after { content: '↕'; font-size: 10px; margin-left: 4px; color: #ccc; }
+        .clickable { color: #4A90E2; cursor: pointer; font-weight: bold; }
+        .num-badge { font-weight: bold; color: #d32f2f; background: #fff1f0; padding: 2px 6px; border-radius: 4px; }
     </style>
 </head>
 <body>
-    <div class="navbar header-subject">
-        <span class="nav-title">章节分析 (顶级知识点)</span>
-    </div>
-
+    <div class="navbar header-subject"><span class="nav-title">章节分析</span></div>
     <div class="sub-container">
-        <!-- 搜索过滤 -->
         <div class="filter-bar">
             <select id="subjectFilter">
-                <option value="全部">全部科目</option>
-                {% for sub in subjects %}
-                <option value="{{ sub.Subject }}">{{ sub.Subject }}</option>
-                {% endfor %}
+                <option value="全部">全部</option>
+                {% for sub in subjects %}<option value="{{ sub.Subject }}">{{ sub.Subject }}</option>{% endfor %}
             </select>
-            <input type="text" id="keyword" placeholder="搜索章节名称...">
-            <button onclick="loadData()">查询</button>
+            <input type="text" id="keyword" placeholder="搜索章节...">
+            <button class="btn-query" onclick="loadData()">查询</button>
+            <a href="/subject/" class="btn-back-nav">返回</a>
         </div>
 
-        <!-- 数据表格 -->
         <div class="table-container">
-            <table id="chapterTable">
+            <table>
                 <thead>
                     <tr>
-                        <th>科目</th>
-                        <th>章节名称</th>
-                        <th style="text-align: center;">错题总数</th>
-                        <th style="text-align: center;">尚未正确</th>
+                        <th onclick="sortTable('SubjectName')">科目</th>
+                        <th onclick="sortTable('ChapterDesc')">章节</th>
+                        <th onclick="sortTable('total')">总数</th>
+                        <th onclick="sortTable('not_correct')">待完成</th>
                     </tr>
                 </thead>
-                <tbody id="chapterBody">
-                    <tr>
-                        <td colspan="4" class="loading-tip">正在加载数据...</td>
-                    </tr>
-                </tbody>
+                <tbody id="chapterBody"></tbody>
             </table>
         </div>
-
-        <div style="text-align: center; margin-top: 30px;">
-            <a href="/subject/" class="back-btn">返回科目首页</a>
-        </div>
     </div>
 
     <script>
+        let currentData = [];
+        let sortDirection = -1;
+
         async function loadData() {
-            const tbody = document.getElementById('chapterBody');
             const sub = document.getElementById('subjectFilter').value;
             const key = document.getElementById('keyword').value;
-            
+            const tbody = document.getElementById('chapterBody');
+            tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:20px;">加载中...</td></tr>';
             try {
-                const res = await fetch(`/api/chapters?subject=${encodeURIComponent(sub)}&keyword=${encodeURIComponent(key)}`);
-                const data = await res.json();
-                
-                if (data.length === 0) {
-                    tbody.innerHTML = '<tr><td colspan="4" class="loading-tip">无相关章节统计数据</td></tr>';
-                    return;
-                }
-
-                tbody.innerHTML = data.map(row => `
-                    <tr>
-                        <td>${row.SubjectName}</td>
-                        <td class="clickable" onclick="location.href='/subject/knowledge?chapter_id=${row.chapter_id}'">
-                            \${row.ChapterDesc}
-                        </td>
-                        <td style="text-align: center;">
-                            <span class="total-badge">${row.total}</span>
-                        </td>
-                        <td style="text-align: center;">
-                            <span class="num-badge">${row.not_correct}</span>
-                        </td>
-                    </tr>
-                `).join('');
+                const response = await fetch('/api/chapters?subject=' + encodeURIComponent(sub) + '&keyword=' + encodeURIComponent(key));
+                currentData = await response.json();
+                currentData.sort((a, b) => b.not_correct - a.not_correct);
+                renderTable(currentData);
             } catch (err) {
-                console.error(err);
-                tbody.innerHTML = '<tr><td colspan="4" class="loading-tip" style="color:red;">加载失败,请检查数据库连接</td></tr>';
+                tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:red;">加载失败</td></tr>';
             }
         }
 
-        // 初始加载
+        function sortTable(column) {
+            sortDirection *= -1;
+            currentData.sort((a, b) => {
+                let valA = a[column] || '';
+                let valB = b[column] || '';
+                return typeof valA === 'string' 
+                    ? sortDirection === 1 ? valA.localeCompare(valB) : valB.localeCompare(valA)
+                    : sortDirection === 1 ? valA - valB : valB - valA;
+            });
+            renderTable(currentData);
+        }
+
+        function renderTable(data) {
+            const tbody = document.getElementById('chapterBody');
+            if (!data || data.length === 0) {
+                tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:20px;">暂无数据</td></tr>';
+                return;
+            }
+            tbody.innerHTML = data.map(row => `
+                <tr>
+                    <td>${row.SubjectName || '-'}</td>
+                    <td class="clickable" onclick="location.href='/subject/knowledge?chapter_id=${row.chapter_id}'">${row.ChapterDesc || '未命名'}</td>
+                    <td>${row.total || 0}</td>
+                    <td><span class="num-badge">${row.not_correct || 0}</span></td>
+                </tr>
+            `).join('');
+        }
         window.onload = loadData;
     </script>
 </body>
index 80dc95bfebddc0ba3bea96cdc49c77d84de72697..e2197cc8140cb987018c23a03bcc3ff1b1b9051e 100644 (file)
 <!DOCTYPE html>
-<html>
+<html lang="zh-CN">
 <head>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="/static/css/style.css">
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <title>知识点分析</title>
+    <link rel="stylesheet" href="/static/css/style.css">
     <style>
-        .filter-bar { background: white; padding: 15px; margin-bottom: 20px; border-radius: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
-        table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; }
-        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; }
-        th { background: #52C41A; color: white; }
+        .filter-bar { background: white; padding: 12px; margin-bottom: 15px; border-radius: 12px; display: flex; gap: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); align-items: center; }
+        #subjectFilter { width: 90px; flex: none; padding: 8px 4px; border: 1px solid #ddd; border-radius: 6px; }
+        #keyword { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; min-width: 0; }
+        .btn-query { background: #4A90E2; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; white-space: nowrap; }
+        .btn-back-nav { background: #f0f0f0; color: #666; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; white-space: nowrap; text-decoration: none; font-size: 14px; }
+        
+        .table-container { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 50px; }
+        table { width: 100%; border-collapse: collapse; }
+        th, td { padding: 12px 10px; text-align: left; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
+        th { background-color: #f8f9fa; color: #666; font-weight: 600; cursor: pointer; position: relative; }
+        th:after { content: '↕'; font-size: 10px; margin-left: 4px; color: #ccc; }
+        .num-badge { font-weight: bold; color: #d32f2f; background: #fff1f0; padding: 2px 6px; border-radius: 4px; }
     </style>
 </head>
 <body>
-    <div class="navbar header-question" style="background: linear-gradient(135deg, #52C41A 0%, #389e0d 100%);">知识点分析</div>
+    <div class="navbar header-subject"><span class="nav-title">知识点分析</span></div>
     <div class="sub-container">
         <div class="filter-bar">
             <select id="subjectFilter">
-                <option value="全部">全部科目</option>
-                {% for sub in subjects %}
-                <option value="{{ sub.Subject }}">{{ sub.Subject }}</option>
-                {% endfor %}
+                <option value="全部">全部</option>
+                {% for sub in subjects %}<option value="{{ sub.Subject }}">{{ sub.Subject }}</option>{% endfor %}
             </select>
             <input type="text" id="keyword" placeholder="搜索知识点...">
-            <button onclick="loadData()">查询</button>
+            <button class="btn-query" onclick="loadData()">查询</button>
+            <a href="/subject/chapters" class="btn-back-nav">返回</a>
         </div>
 
-        <table id="kpTable">
-            <thead>
-                <tr>
-                    <th>科目</th>
-                    <th>知识点内容</th>
-                    <th>总题数</th>
-                    <th>待完成</th>
-                </tr>
-            </thead>
-            <tbody></tbody>
-        </table>
-        <a href="/subject/" class="back-btn">返回科目首页</a>
+        <div class="table-container">
+            <table>
+                <thead>
+                    <tr>
+                        <th onclick="sortTable('subject_name')">科目</th>
+                        <th onclick="sortTable('content')">知识点内容</th>
+                        <th onclick="sortTable('total')">总数</th>
+                        <th onclick="sortTable('not_correct')">待完成</th>
+                    </tr>
+                </thead>
+                <tbody id="knowledgeBody"></tbody>
+            </table>
+        </div>
     </div>
 
     <script>
-        const urlParams = new URLSearchParams(window.location.search);
-        const chapterId = urlParams.get('chapter_id') || '';
+        let currentData = [];
+        let sortDirection = -1;
+        const chapterId = "{{ chapter_id }}";
 
         async function loadData() {
             const sub = document.getElementById('subjectFilter').value;
             const key = document.getElementById('keyword').value;
-            const res = await fetch(`/api/knowledge?subject=${sub}&keyword=${key}&chapter_id=${chapterId}`);
-            const data = await res.json();
-            const tbody = document.querySelector('#kpTable tbody');
+            const tbody = document.getElementById('knowledgeBody');
+            tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:20px;">加载中...</td></tr>';
+            try {
+                const response = await fetch(`/api/knowledge?subject=${encodeURIComponent(sub)}&keyword=${encodeURIComponent(key)}&chapter_id=${chapterId}`);
+                currentData = await response.json();
+                currentData.sort((a, b) => b.not_correct - a.not_correct);
+                renderTable(currentData);
+            } catch (err) {
+                tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:red;">加载失败</td></tr>';
+            }
+        }
+
+        function sortTable(column) {
+            sortDirection *= -1;
+            currentData.sort((a, b) => {
+                let valA = a[column] || '';
+                let valB = b[column] || '';
+                return typeof valA === 'string'
+                    ? sortDirection === 1 ? valA.localeCompare(valB) : valB.localeCompare(valA)
+                    : sortDirection === 1 ? valA - valB : valB - valA;
+            });
+            renderTable(currentData);
+        }
+
+        function renderTable(data) {
+            const tbody = document.getElementById('knowledgeBody');
+            if (!data || data.length === 0) {
+                tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:20px;">暂无数据</td></tr>';
+                return;
+            }
             tbody.innerHTML = data.map(row => `
                 <tr>
-                    <td>${row.subject_name}</td>
-                    <td>${row.content}</td>
-                    <td>${row.total}</td>
-                    <td style="color:red; font-weight:bold;">${row.not_correct}</td>
+                    <td>${row.subject_name || '-'}</td>
+                    <td>${row.content || '-'}</td>
+                    <td>${row.total || 0}</td>
+                    <td><span class="num-badge">${row.not_correct || 0}</span></td>
                 </tr>
             `).join('');
         }
-        loadData();
+        window.onload = loadData;
     </script>
 </body>
 </html>