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')
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)
--- /dev/null
+<!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>
<!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>
--- /dev/null
+<!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>