]> acesimba.cloud Git - studybase.git/commitdiff
update: 添加科目首页分析功能
authorroot <root@localhost.localdomain>
Mon, 18 May 2026 22:38:22 +0000 (06:38 +0800)
committerroot <root@localhost.localdomain>
Mon, 18 May 2026 22:38:22 +0000 (06:38 +0800)
app.py
templates/subject/index.html

diff --git a/app.py b/app.py
index e9f9dacb1c18b00d7e15cd08628f6ea69248bd9b..00ce6559fb12e2eab4e03e7d87c4f10d23ee88f5 100644 (file)
--- a/app.py
+++ b/app.py
@@ -1,55 +1,35 @@
-from flask import Flask, render_template
-import os
-from database import query_db, execute_db
+from flask import Flask, render_template, request, jsonify
+from database import query_db
 
 app = Flask(__name__)
 
-# 配置:确保 Flask 能正确找到模板和静态文件
-app.config['SECRET_KEY'] = 'studybase_secret_key_2026'
-
 # 1. 首页
 @app.route('/')
 def index():
     return render_template('index.html')
 
-# 2. 科目面板
+# 2. 科目面板 (注意:路由加了斜杠,匹配你的访问日志)
 @app.route('/subject/')
-def subject():
-    # 这里的 SQL 逻辑:
-    # 1. 从 Subject 表查询所有科目
-    # 2. 关联 questions 表统计每个科目的题目数量
-    # 3. 过滤 enabled=1 的科目
-    query = """
-        SELECT 
-            s.ID, 
-            s.Subject as name, 
-            s.CreateTime as updated_at,
-            (SELECT COUNT(*) FROM questions q WHERE q.subject_id = s.ID) as q_count
-        FROM Subject s
-        WHERE s.enabled = 1
-        ORDER BY s.ID ASC
-    """
+def subject_portal():
     try:
-        subjects = query_db(query)
+        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:
-        print(f"数据库查询出错: {e}")
-        subjects = []
-        
-    return render_template('subject/index.html', subjects=subjects)
+        return f"数据库连接失败: {e}"
 
 # 3. 题目管理
 @app.route('/questions/')
 def questions():
-    # 示例:读取最近的10道题目
-    # questions = query_db('SELECT * FROM questions LIMIT 10')
-    # return render_template('questions/index.html', questions=questions)
     return render_template('questions/index.html')
 
 # 4. 试卷管理
 @app.route('/papers/')
 def papers():
-    # 示例:读取所有试卷列表
-    # papers = query_db('SELECT * FROM papers')
     return render_template('papers/index.html')
 
 # 5. 系统设定
@@ -57,12 +37,48 @@ def papers():
 def settings():
     return render_template('settings/index.html')
 
-# 错误处理:404 页面返回首页
-@app.errorhandler(404)
-def page_not_found(e):
-    return render_template('index.html'), 404
+# 6. 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
+
+    # 构造阶段过滤 SQL
+    stage_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
+
+    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
+        }
+    })
 
 if __name__ == '__main__':
-    # debug=True 允许热更新,修改代码后保存即可生效
-    # host='0.0.0.0' 允许通过公网 IP 或局域网访问
-    app.run(host='0.0.0.0', port=5000, debug=True)
+    # 开启 debug=True,这样报错会有详细提示
+    app.run(host='0.0.0.0', port=5000, debug=True, threaded=True)
index 0a53198a7a2121c5d08d8f7e0eb872af4585b72c..0b4c63a4dd9ebb06de2ef1b42d172a5898ce9b68 100644 (file)
 <html lang="zh">
 <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>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>科目概要面板</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
     <style>
-        /* 针对科目页面的局部样式微调 */
-        .content-area {
-            max-width: 1000px;
-            margin: 20px auto;
-            padding: 0 15px;
-        }
+        .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: repeat(auto-fit, minmax(140px, 1fr));
-            gap: 15px;
-            margin-bottom: 25px;
-        }
-
-        .stat-card {
-            background: white;
-            padding: 15px;
-            border-radius: 12px;
-            text-align: center;
-            box-shadow: 0 2px 8px rgba(0,0,0,0.05);
-            border-bottom: 4px solid #0066cc;
-        }
-
-        .stat-card .num {
-            display: block;
-            font-size: 24px;
-            font-weight: bold;
-            color: #0066cc;
-        }
-
-        .stat-card .label {
-            font-size: 13px;
-            color: #888;
-            margin-top: 5px;
-        }
-
-        .data-table-container {
-            background: white;
-            border-radius: 12px;
-            overflow: hidden;
-            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
-        }
-
-        table {
-            width: 100%;
-            border-collapse: collapse;
-        }
-
-        th {
-            background-color: #f8f9fa;
-            color: #666;
-            font-size: 14px;
-            font-weight: 600;
-            padding: 15px 12px;
-            text-align: left;
-            border-bottom: 2px solid #eee;
-        }
-
-        td {
-            padding: 15px 12px;
-            border-bottom: 1px solid #eee;
-            color: #444;
-            font-size: 15px;
-        }
-
-        tr:last-child td {
-            border-bottom: none;
-        }
-
-        .subject-name {
-            display: flex;
-            align-items: center;
-            font-weight: 600;
-            color: #333;
-        }
-
-        .id-tag {
-            font-size: 11px;
-            color: #999;
-            margin-right: 8px;
-            background: #f0f0f0;
-            padding: 2px 6px;
-            border-radius: 4px;
-        }
-
-        .q-count-badge {
-            background: #eef2f7;
-            padding: 4px 12px;
-            border-radius: 20px;
-            color: #0066cc;
-            font-size: 13px;
-            font-weight: bold;
-        }
-
-        .action-btn {
-            padding: 6px 12px;
-            border-radius: 6px;
-            font-size: 13px;
-            text-decoration: none;
-            background: #0066cc;
-            color: white;
-            transition: opacity 0.2s;
-        }
-
-        .action-btn:hover {
-            opacity: 0.8;
-        }
-
-        /* 移动端优化:iPhone 竖屏 */
-        @media (max-width: 600px) {
-            th:nth-child(3), td:nth-child(3) { display: none; } /* 隐藏时间列 */
-            .stat-card .num { font-size: 20px; }
-            td { padding: 12px 8px; }
-            .action-btn { padding: 5px 8px; font-size: 12px; }
-        }
+        .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; }
     </style>
 </head>
-<body>
-    <!-- 使用我们定义的蓝色渐变 Header -->
-    <div class="navbar header-subject">科目面板</div>
-
-    <div class="content-area">
-        <!-- 顶部统计信息 -->
-        <div class="stats-grid">
-            <div class="stat-card">
-                <span class="num">{{ subjects|length if subjects else 0 }}</span>
-                <span class="label">活跃科目</span>
+<body style="background: #F0F2F5;">
+    <div class="navbar header-subject">科目概要面板</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;">
+                    {% for sub in subjects %}
+                    <option value="{{ sub.ID }}" {% if sub.ID == default_id %}selected{% endif %}>{{ sub.name }}</option>
+                    {% endfor %}
+                </select>
             </div>
-            <div class="stat-card">
-                <span class="num">
-                    {% set total_q = 0 %}
-                    {% if subjects %}
-                        {% for sub in subjects %}
-                            {% set total_q = total_q + sub.q_count %}
-                        {% endfor %}
-                    {% endif %}
-                    {{ total_q }}
-                </span>
-                <span class="label">错题总数</span>
+            <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 %}
+                </div>
             </div>
         </div>
 
-        <!-- 数据表格 -->
-        <div class="data-table-container">
-            <table>
-                <thead>
-                    <tr>
-                        <th>科目名称</th>
-                        <th>题目数</th>
-                        <th>创建时间</th>
-                        <th>管理</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {% if subjects %}
-                        {% for sub in subjects %}
-                        <tr>
-                            <td>
-                                <div class="subject-name">
-                                    <span class="id-tag">ID:{{ sub.ID }}</span>
-                                    {{ sub.name }}
-                                </div>
-                            </td>
-                            <td>
-                                <span class="q-count-badge">{{ sub.q_count }} 题</span>
-                            </td>
-                            <td>
-                                <span style="font-size: 12px; color: #999;">{{ sub.updated_at }}</span>
-                            </td>
-                            <td>
-                                <a href="/questions/?subject_id={{ sub.ID }}" class="action-btn">查看</a>
-                            </td>
-                        </tr>
-                        {% endfor %}
-                    {% else %}
-                        <tr>
-                            <td colspan="4" style="text-align: center; padding: 50px; color: #999;">
-                                <div style="font-size: 40px; margin-bottom: 15px;">🔍</div>
-                                暂无科目数据<br>
-                                <small>请检查数据库中的 Subject 表是否已录入内容</small>
-                            </td>
-                        </tr>
-                    {% endif %}
-                </tbody>
-            </table>
+        <!-- 统计卡片区 -->
+        <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>
+
+            <!-- 推题卡片 -->
+            <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>
         </div>
 
-        <!-- 返回导航 -->
         <div style="text-align: center; margin-top: 30px;">
-            <a href="/" class="back-btn">返回系统首页</a>
+            <a href="/" class="back-btn">返回首页</a>
         </div>
     </div>
+
+    <script>
+        function refreshStats() {
+            const subId = document.getElementById('subjectSelect').value;
+            const stageIds = Array.from(document.querySelectorAll('input[name="stage"]:checked')).map(cb => cb.value);
+            
+            if (stageIds.length === 0) {
+                updateUI({wrong: {total:0, fixed:0, ratio:'0%', remained:0}, push: {total:0, fixed:0, ratio:'0%', remained:0}});
+                return;
+            }
+
+            fetch(`/api/subject_stats?subject_id=${subId}&stage_ids=${stageIds.join(',')}`)
+                .then(response => response.json())
+                .then(data => updateUI(data));
+        }
+
+        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;
+
+            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;
+        }
+
+        // 页面加载完成后立即刷新一次
+        window.onload = refreshStats;
+    </script>
 </body>
 </html>