-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. 系统设定
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)
<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>