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")
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)
<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>
<!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>