]> acesimba.cloud Git - studybase.git/commitdiff
update: 章节首页为空问题修复,但是打开后章节名称依然是错误的;知识点首页目前依然显示为空
authorroot <root@localhost.localdomain>
Tue, 19 May 2026 00:03:24 +0000 (08:03 +0800)
committerroot <root@localhost.localdomain>
Tue, 19 May 2026 00:03:24 +0000 (08:03 +0800)
app.py
templates/subject/chapters.html [new file with mode: 0644]
templates/subject/index.html
templates/subject/knowledge.html [new file with mode: 0644]

diff --git a/app.py b/app.py
index 00ce6559fb12e2eab4e03e7d87c4f10d23ee88f5..8c80782d942f5b21c3b44f147fa68efe08485e62 100644 (file)
--- a/app.py
+++ b/app.py
@@ -3,61 +3,59 @@ from database import query_db
 
 app = Flask(__name__)
 
-# 1. 首页
+# --- 1. 页面路由 ---
+
 @app.route('/')
 def index():
     return render_template('index.html')
 
-# 2. 科目面板 (注意:路由加了斜杠,匹配你的访问日志)
+# 科目首页
 @app.route('/subject/')
 def subject_portal():
-    try:
-        subjects = query_db("SELECT ID, Subject as name FROM Subject WHERE enabled = 1")
-        stages = query_db("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID")
-        default_subject_id = subjects[0]['ID'] if subjects else None
-        return render_template('subject/index.html', 
-                               subjects=subjects, 
-                               stages=stages, 
-                               default_id=default_subject_id)
-    except Exception as e:
-        return f"数据库连接失败: {e}"
+    subjects = query_db("SELECT ID, Subject as name FROM Subject WHERE enabled = 1")
+    stages = query_db("SELECT ID, stage_name FROM stage WHERE enabled = 1 ORDER BY ID")
+    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)
 
-# 3. 题目管理
 @app.route('/questions/')
-def questions():
-    return render_template('questions/index.html')
+def questions(): return render_template('questions/index.html')
 
-# 4. 试卷管理
 @app.route('/papers/')
-def papers():
-    return render_template('papers/index.html')
+def papers(): return render_template('papers/index.html')
 
-# 5. 系统设定
 @app.route('/settings/')
-def settings():
-    return render_template('settings/index.html')
+def settings(): return render_template('settings/index.html')
 
-# 6. 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
+        return jsonify({"error": "参数错误"}), 400
 
-    # 构造阶段过滤 SQL
-    stage_placeholders = ','.join(['?'] * len(stage_ids))
+    placeholders = ','.join(['?'] * len(stage_ids))
     
-    def get_count(q_type, extra_condition=""):
-        sql = f"""
-            SELECT COUNT(*) as count FROM questions 
-            WHERE question_type = ? AND subject_id = ? 
-            AND stage_id IN ({stage_placeholders}) {extra_condition}
-        """
-        params = [q_type, sub_id] + stage_ids
-        res = query_db(sql, params, one=True)
-        return res['count'] if res else 0
+    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)
+        return res['c'] if res else 0
 
     w_total = get_count('错题')
     w_fixed = get_count('错题', 'AND right_times > 0')
@@ -65,20 +63,98 @@ def get_subject_stats():
     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
-        }
+        "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}
     })
 
+# 章节数据 API (基于顶级知识点)
+@app.route('/api/chapters')
+def get_chapters():
+    subject = 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)
+    if keyword:
+        where += " AND kp.content 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}
+    """
+    try:
+        rows = query_db(sql, params)
+        result = []
+        for row in rows:
+            item = dict(row) # 必须转字典,Row 不支持赋值
+            item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0)
+            # 只要有题目就显示
+            if item['total'] > 0:
+                result.append(item)
+        return jsonify(result)
+    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', '全部')
+    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)
+    if keyword:
+        where += " AND kp.content LIKE ?"
+        params.append(f"%{keyword}%")
+
+    sql = f"""
+    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}
+    """
+    try:
+        rows = query_db(sql, params)
+        result = []
+        for row in rows:
+            item = dict(row)
+            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__':
-    # 开启 debug=True,这样报错会有详细提示
-    app.run(host='0.0.0.0', port=5000, debug=True, threaded=True)
+    # 确保在服务器上运行时监听 0.0.0.0
+    app.run(host='0.0.0.0', port=5000, debug=True)
diff --git a/templates/subject/chapters.html b/templates/subject/chapters.html
new file mode 100644 (file)
index 0000000..b7d5cba
--- /dev/null
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <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: 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;
+        }
+    </style>
+</head>
+<body>
+    <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 %}
+            </select>
+            <input type="text" id="keyword" placeholder="搜索章节名称...">
+            <button onclick="loadData()">查询</button>
+        </div>
+
+        <!-- 数据表格 -->
+        <div class="table-container">
+            <table id="chapterTable">
+                <thead>
+                    <tr>
+                        <th>科目</th>
+                        <th>章节名称</th>
+                        <th style="text-align: center;">错题总数</th>
+                        <th style="text-align: center;">尚未正确</th>
+                    </tr>
+                </thead>
+                <tbody id="chapterBody">
+                    <tr>
+                        <td colspan="4" class="loading-tip">正在加载数据...</td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+
+        <div style="text-align: center; margin-top: 30px;">
+            <a href="/subject/" class="back-btn">返回科目首页</a>
+        </div>
+    </div>
+
+    <script>
+        async function loadData() {
+            const tbody = document.getElementById('chapterBody');
+            const sub = document.getElementById('subjectFilter').value;
+            const key = document.getElementById('keyword').value;
+            
+            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('');
+            } catch (err) {
+                console.error(err);
+                tbody.innerHTML = '<tr><td colspan="4" class="loading-tip" style="color:red;">加载失败,请检查数据库连接</td></tr>';
+            }
+        }
+
+        // 初始加载
+        window.onload = loadData;
+    </script>
+</body>
+</html>
index 0b4c63a4dd9ebb06de2ef1b42d172a5898ce9b68..8ec125e1649021e1e63adab106fa11820c356f47 100644 (file)
 <!DOCTYPE html>
-<html lang="zh">
+<html lang="zh-CN">
 <head>
     <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <title>科目概要面板</title>
-    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
+    <link rel="stylesheet" href="/static/css/style.css">
     <style>
-        .portal-container { max-width: 800px; margin: 0 auto; padding: 15px; }
-        .control-panel { background: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
-        .form-group { margin-bottom: 15px; }
-        .checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
-        .checkbox-item { background: #f0f2f5; padding: 5px 12px; border-radius: 20px; font-size: 14px; cursor: pointer; }
-        .checkbox-item input { margin-right: 5px; }
-
-        /* 统计卡片布局 - 适配 iPhone */
-        .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
-        @media (max-width: 600px) { .stats-grid { grid-template-columns: 1fr; } }
-
-        .stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-top: 5px solid #eee; }
-        .card-wrong { border-top-color: #E64A19; }
-        .card-push { border-top-color: #00796B; }
+        /* 针对统计页面的微调 */
+        .stats-grid {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 15px;
+            margin-bottom: 20px;
+        }
+        @media (max-width: 600px) {
+            .stats-grid { grid-template-columns: 1fr; }
+        }
+        .stat-card {
+            background: white;
+            padding: 15px;
+            border-radius: 12px;
+            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
+            border-top: 4px solid #4A90E2;
+        }
+        .stat-card.push { border-top-color: #52C41A; }
+        .stat-card h3 { margin: 0 0 10px 0; font-size: 16px; color: #333; }
+        .stat-line { display: flex; justify-content: space-between; margin: 8px 0; font-size: 14px; color: #666; }
+        .stat-value { font-weight: bold; color: #333; }
+        .stat-ratio { 
+            font-size: 24px; 
+            font-weight: bold; 
+            color: #4A90E2; 
+            text-align: center; 
+            margin: 10px 0;
+            display: block;
+        }
+        .push .stat-ratio { color: #52C41A; }
         
-        .stat-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; display: flex; align-items: center; }
-        .stat-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
-        .stat-label { color: #666; font-size: 14px; }
-        .stat-val { font-family: 'Helvetica', sans-serif; font-weight: bold; font-size: 20px; }
-        .stat-ratio { font-size: 32px; color: #1976D2; display: block; margin: 10px 0; }
+        .filter-section {
+            background: #fff;
+            padding: 15px;
+            border-radius: 12px;
+            margin-bottom: 20px;
+        }
+        .stage-chips {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-top: 10px;
+        }
+        .stage-chip {
+            padding: 5px 12px;
+            border-radius: 15px;
+            background: #f0f2f5;
+            font-size: 13px;
+            cursor: pointer;
+            border: 1px solid #ddd;
+        }
+        .stage-chip.active {
+            background: #4A90E2;
+            color: white;
+            border-color: #4A90E2;
+        }
+        .analysis-btn-group {
+            display: flex;
+            gap: 15px;
+            margin: 20px 0;
+        }
+        .analysis-btn-group .card {
+            flex: 1;
+            height: 70px;
+            margin: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 16px;
+            font-weight: bold;
+        }
     </style>
 </head>
-<body style="background: #F0F2F5;">
-    <div class="navbar header-subject">科目概要面板</div>
+<body>
+    <!-- 顶部导航 -->
+    <div class="navbar header-subject">
+        <span class="nav-title">科目概要面板</span>
+    </div>
 
-    <div class="portal-container">
-        <!-- 控制区 -->
-        <div class="control-panel">
-            <div class="form-group">
-                <label>当前科目:</label>
-                <select id="subjectSelect" onchange="refreshStats()" style="padding: 8px; border-radius: 5px; width: 100%; max-width: 200px;">
+    <div class="sub-container">
+        <!-- 1. 筛选区 -->
+        <div class="filter-section">
+            <div style="margin-bottom: 15px;">
+                <label style="font-weight: bold;">当前科目:</label>
+                <select id="subjectSelect" style="width: 100%; padding: 10px; margin-top: 5px; border-radius: 8px; border: 1px solid #ddd;">
                     {% for sub in subjects %}
                     <option value="{{ sub.ID }}" {% if sub.ID == default_id %}selected{% endif %}>{{ sub.name }}</option>
                     {% endfor %}
                 </select>
             </div>
-            <div class="form-group">
-                <label>统计阶段:</label>
-                <div class="checkbox-group">
-                    {% for stage in stages %}
-                    <label class="checkbox-item">
-                        <input type="checkbox" name="stage" value="{{ stage.ID }}" checked onchange="refreshStats()"> {{ stage.stage_name }}
-                    </label>
-                    {% endfor %}
+            
+            <label style="font-weight: bold;">统计阶段:</label>
+            <div class="stage-chips" id="stageContainer">
+                {% for stage in stages %}
+                <div class="stage-chip active" data-id="{{ stage.ID }}" onclick="toggleStage(this)">
+                    {{ stage.stage_name }}
                 </div>
+                {% endfor %}
             </div>
         </div>
 
-        <!-- 统计卡片区 -->
+        <!-- 2. 统计卡片区 -->
         <div class="stats-grid">
             <!-- 错题卡片 -->
-            <div class="stat-card card-wrong">
-                <div class="stat-title" style="color: #E64A19;">错题情况回顾</div>
-                <div class="stat-line"><span class="stat-label">错题总数</span><span class="stat-val" id="w_total">0</span></div>
-                <div class="stat-line"><span class="stat-label">已解决数</span><span class="stat-val" style="color: #2E7D32;" id="w_fixed">0</span></div>
-                <div style="text-align: center; border-top: 1px solid #eee; margin-top: 10px;">
-                    <span class="stat-label">解决比例</span>
-                    <span class="stat-ratio" id="w_ratio">0%</span>
-                </div>
-                <div class="stat-line"><span class="stat-label">遗留题目数</span><span class="stat-val" style="color: #D32F2F;" id="w_remained">0</span></div>
+            <div class="stat-card">
+                <h3>错题情况回顾</h3>
+                <span class="stat-ratio" id="wrong_ratio">0%</span>
+                <div class="stat-line"><span>总数</span><span class="stat-value" id="wrong_total">0</span></div>
+                <div class="stat-line"><span>已解决</span><span class="stat-value" id="wrong_fixed" style="color: #2E7D32;">0</span></div>
+                <div class="stat-line"><span>遗留</span><span class="stat-value" id="wrong_remained" style="color: #D32F2F;">0</span></div>
             </div>
 
             <!-- 推题卡片 -->
-            <div class="stat-card card-push">
-                <div class="stat-title" style="color: #00796B;">强化推题追踪</div>
-                <div class="stat-line"><span class="stat-label">推题总数</span><span class="stat-val" id="p_total">0</span></div>
-                <div class="stat-line"><span class="stat-label">已完成数</span><span class="stat-val" style="color: #2E7D32;" id="p_fixed">0</span></div>
-                <div style="text-align: center; border-top: 1px solid #eee; margin-top: 10px;">
-                    <span class="stat-label">完成比例</span>
-                    <span class="stat-ratio" style="color: #F57C00;" id="p_ratio">0%</span>
-                </div>
-                <div class="stat-line"><span class="stat-label">遗留题目数</span><span class="stat-val" style="color: #D32F2F;" id="p_remained">0</span></div>
+            <div class="stat-card push">
+                <h3>强化推题追踪</h3>
+                <span class="stat-ratio" id="push_ratio">0%</span>
+                <div class="stat-line"><span>总数</span><span class="stat-value" id="push_total">0</span></div>
+                <div class="stat-line"><span>已完成</span><span class="stat-value" id="push_fixed" style="color: #2E7D32;">0</span></div>
+                <div class="stat-line"><span>遗留</span><span class="stat-value" id="push_remained" style="color: #D32F2F;">0</span></div>
             </div>
         </div>
 
+        <!-- 3. 深入分析按钮区 -->
+        <div class="analysis-btn-group">
+            <a href="/subject/chapters" class="card card-subject">
+                📊 章节分析
+            </a>
+            <a href="/subject/knowledge" class="card card-question">
+                💡 知识点分析
+            </a>
+        </div>
+
+        <!-- 4. 返回按钮 -->
         <div style="text-align: center; margin-top: 30px;">
             <a href="/" class="back-btn">返回首页</a>
         </div>
     </div>
 
     <script>
-        function refreshStats() {
+        // 切换阶段选中状态
+        function toggleStage(el) {
+            el.classList.toggle('active');
+            refreshStats();
+        }
+
+        // 获取当前选中的阶段ID数组
+        function getSelectedStages() {
+            const actives = document.querySelectorAll('.stage-chip.active');
+            return Array.from(actives).map(el => el.dataset.id);
+        }
+
+        // 异步刷新数据
+        async function refreshStats() {
             const subId = document.getElementById('subjectSelect').value;
-            const stageIds = Array.from(document.querySelectorAll('input[name="stage"]:checked')).map(cb => cb.value);
+            const stages = getSelectedStages().join(',');
             
-            if (stageIds.length === 0) {
-                updateUI({wrong: {total:0, fixed:0, ratio:'0%', remained:0}, push: {total:0, fixed:0, ratio:'0%', remained:0}});
+            if (!stages) {
+                updateUI(null);
                 return;
             }
 
-            fetch(`/api/subject_stats?subject_id=${subId}&stage_ids=${stageIds.join(',')}`)
-                .then(response => response.json())
-                .then(data => updateUI(data));
+            try {
+                const response = await fetch(`/api/subject_stats?subject_id=${subId}&stage_ids=${stages}`);
+                const data = await response.json();
+                updateUI(data);
+            } catch (error) {
+                console.error("加载数据失败:", error);
+            }
         }
 
         function updateUI(data) {
-            document.getElementById('w_total').innerText = data.wrong.total;
-            document.getElementById('w_fixed').innerText = data.wrong.fixed;
-            document.getElementById('w_ratio').innerText = data.wrong.ratio;
-            document.getElementById('w_remained').innerText = data.wrong.remained;
+            if (!data) {
+                const keys = ['wrong_total', 'wrong_fixed', 'wrong_remained', 'wrong_ratio', 
+                             'push_total', 'push_fixed', 'push_remained', 'push_ratio'];
+                keys.forEach(k => document.getElementById(k).innerText = k.includes('ratio') ? '0%' : '0');
+                return;
+            }
+            
+            document.getElementById('wrong_total').innerText = data.wrong.total;
+            document.getElementById('wrong_fixed').innerText = data.wrong.fixed;
+            document.getElementById('wrong_remained').innerText = data.wrong.remained;
+            document.getElementById('wrong_ratio').innerText = data.wrong.ratio;
 
-            document.getElementById('p_total').innerText = data.push.total;
-            document.getElementById('p_fixed').innerText = data.push.fixed;
-            document.getElementById('p_ratio').innerText = data.push.ratio;
-            document.getElementById('p_remained').innerText = data.push.remained;
+            document.getElementById('push_total').innerText = data.push.total;
+            document.getElementById('push_fixed').innerText = data.push.fixed;
+            document.getElementById('push_remained').innerText = data.push.remained;
+            document.getElementById('push_ratio').innerText = data.push.ratio;
         }
 
-        // 页面加载完成后立即刷新一次
+        // 监听科目切换
+        document.getElementById('subjectSelect').addEventListener('change', refreshStats);
+
+        // 页面加载时自动执行一次
         window.onload = refreshStats;
     </script>
 </body>
diff --git a/templates/subject/knowledge.html b/templates/subject/knowledge.html
new file mode 100644 (file)
index 0000000..80dc95b
--- /dev/null
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="/static/css/style.css">
+    <title>知识点分析</title>
+    <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; }
+    </style>
+</head>
+<body>
+    <div class="navbar header-question" style="background: linear-gradient(135deg, #52C41A 0%, #389e0d 100%);">知识点分析</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 %}
+            </select>
+            <input type="text" id="keyword" placeholder="搜索知识点...">
+            <button onclick="loadData()">查询</button>
+        </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>
+
+    <script>
+        const urlParams = new URLSearchParams(window.location.search);
+        const chapterId = urlParams.get('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');
+            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>
+                </tr>
+            `).join('');
+        }
+        loadData();
+    </script>
+</body>
+</html>