-==================================================
+==================================================
第一部分:项目目录结构
==================================================
./
.gitignore
+ app.log
app.py
+ database.py
generate_report.py
custom/
custom/conf/
index.html
templates/questions/
index.html
+ question_list.html
templates/papers/
index.html
templates/subject/
+ chapters.html
index.html
+ knowledge.html
+ _clipboard_temp/
static/
static/css/
style.css
+ s/
+ schema.sql
+ studybase.db
+ studybase.sqbpro
+ workmode-studybase.sql
==================================================
第二部分:文件内容整合
==================================================
->>> FILE START: .gitignore <<<
-========================================
-__pycache__/
-*.py[cod]
-*$py.class
-.venv/
-venv/
-ENV/
-*.db
-.DS_Store
-========================================
->>> FILE END: .gitignore <<<
-
-
>>> FILE START: app.py <<<
========================================
-from flask import Flask, render_template
+from flask import Flask, render_template, request, jsonify
+from database import query_db
app = Flask(__name__)
-# 首页
+# ==================== 1. 核心页面路由 ====================
+
@app.route('/')
def index():
return render_template('index.html')
-# 科目面板
-@app.route('/subject/')
-def subject():
- return render_template('subject/index.html')
-
-# 题目管理
+# --- 题目列表模块 ---
@app.route('/questions/')
-def questions():
+def questions_list():
return render_template('questions/index.html')
-# 试卷管理
+# --- 试卷列表模块 ---
@app.route('/papers/')
-def papers():
+def papers_list():
return render_template('papers/index.html')
-# 系统设定
+# --- 系统设定模块 ---
@app.route('/settings/')
-def 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")
+ 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)
+
+
+# ==================== 2. API 数据接口 ====================
+
+# 科目概览统计
+@app.route('/api/subject_stats')
+def get_subject_stats():
+ sub_id = request.args.get('subject_id')
+ 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=""):
+ 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
+
+ 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
+
+# 章节数据接口 (已适配 chapter_desc)
+@app.route('/api/chapters')
+def get_chapters():
+ subject_name = request.args.get('subject', '全部')
+ keyword = request.args.get('keyword', '')
+ params = []
+ where = " WHERE 1=1"
+ if subject_name != '全部':
+ where += " AND s.Subject = ?"; params.append(subject_name)
+ if keyword:
+ where += " AND c.chapter_desc LIKE ?"; params.append(f"%{keyword}%")
+
+ sql = f"""
+ 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 = [dict(row) for row in rows]
+ for item in result:
+ item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0)
+ return jsonify([i for i in result if i['total'] > 0])
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+# 知识点数据接口
+@app.route('/api/knowledge')
+def get_knowledge():
+ 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:
+ 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}%")
+
+ 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 = [dict(row) for row in rows]
+ for item in result:
+ item['not_correct'] = (item['total'] or 0) - (item['correct'] or 0)
+ return jsonify(result)
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
if __name__ == '__main__':
- app.run(host='0.0.0.0', port=5000)
+ app.run(host='0.0.0.0', port=5000, debug=True)
========================================
>>> FILE END: app.py <<<
->>> FILE START: custom/conf/app.ini <<<
+>>> FILE START: database.py <<<
========================================
-[server]
-LOCAL_ROOT_URL = http://localhost:4999/
+import sqlite3
+import os
+
+# 数据库文件的绝对路径
+DB_PATH = os.path.join(os.path.dirname(__file__), 's', 'studybase.db')
+
+def get_db_connection():
+ """获取数据库连接,并将结果集设置为字典模式(方便前端调用)"""
+ conn = sqlite3.connect(DB_PATH)
+ # 这行代码能让你通过 row['column_name'] 访问数据,而不是 row[0]
+ conn.row_factory = sqlite3.Row
+ return conn
+
+def query_db(query, args=(), one=False):
+ """通用的查询函数"""
+ conn = get_db_connection()
+ cur = conn.execute(query, args)
+ rv = cur.fetchall()
+ conn.close()
+ return (rv[0] if rv else None) if one else rv
+
+def execute_db(query, args=()):
+ """通用的增删改函数"""
+ conn = get_db_connection()
+ cur = conn.execute(query, args)
+ conn.commit()
+ conn.close()
+ return cur
========================================
->>> FILE END: custom/conf/app.ini <<<
+>>> FILE END: database.py <<<
>>> FILE START: generate_report.py <<<
EXCLUDE_EXTS = {".exe", ".pyc", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".tar", ".gz"}
# 需要排除的具体文件名
-EXCLUDE_FILES = {OUTPUT_FILENAME, ".DS_Store","gitea"}
+EXCLUDE_FILES = {OUTPUT_FILENAME, ".DS_Store", "gitea"}
# ===========================================
ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
matches.sort()
return matches
+def is_text_file(filepath):
+ """检查文件是否为文本文件"""
+ text_extensions = ['.txt', '.py', '.html', '.css', '.js', '.md'] # 可根据需要扩展
+ _, ext = os.path.splitext(filepath)
+ return ext.lower() in text_extensions
+
def read_file_content(path):
"""尝试多种编码读取文件内容,非文本或无法读取则跳过"""
- encodings = ["utf-8", "utf-8-sig", "gbk", "latin-1"]
+ encodings = ["utf-8-sig", "utf-8", "gbk", "latin-1"] # 优先使用 utf-8-sig
for enc in encodings:
try:
with io.open(path, "r", encoding=enc, errors="strict") as f:
return "[警告:该文件编码不兼容或为二进制格式,已跳过内容提取]"
def generate_report(root, out_path):
- with io.open(out_path, "w", encoding="utf-8") as out:
+ with io.open(out_path, "w", encoding="utf-8-sig") as out: # 使用 utf-8-sig 输出
# 第一部分:目录结构
write_line(out, "==================================================")
write_line(out, "第一部分:项目目录结构")
write_line(out, "[未找到符合条件的可处理文件]")
for rel_path, full_path in all_files:
- write_line(out, f"\n>>> FILE START: {rel_path} <<<")
- write_line(out, "=" * 40)
-
- content = read_file_content(full_path)
- out.write(content)
-
- if not content.endswith("\n"):
- out.write("\n")
- write_line(out, "=" * 40)
- write_line(out, f">>> FILE END: {rel_path} <<<\n")
+ if is_text_file(full_path): # 仅处理文本文件
+ write_line(out, f"\n>>> FILE START: {rel_path} <<<")
+ write_line(out, "=" * 40)
+
+ content = read_file_content(full_path)
+ out.write(content)
+
+ if not content.endswith("\n"):
+ out.write("\n")
+ write_line(out, "=" * 40)
+ write_line(out, f">>> FILE END: {rel_path} <<<\n")
if __name__ == "__main__":
try:
>>> FILE START: static/css/style.css <<<
========================================
-/* 全局样式 */
+/* 全局基础样式 */
body {
margin: 0;
padding: 0;
background-color: #f0f2f5;
- font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft YaHei", sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
}
-/* 顶部导航栏 */
+/* 顶部导航栏基础样式 */
.navbar {
height: 60px;
background: linear-gradient(135deg, #4088d8 0%, #2a69b5 100%);
justify-content: center;
align-items: center;
color: white;
- font-size: 24px;
+ font-size: 22px;
font-weight: bold;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
}
-/* 主体区域 - 设置为不换行,确保在一行显示 */
+/* 子页面特定的 Header 颜色 - 对应首页按钮色系 */
+.header-subject { background: linear-gradient(135deg, #0066cc 0%, #004499 100%) !important; }
+.header-question { background: linear-gradient(135deg, #009944 0%, #007733 100%) !important; }
+.header-paper { background: linear-gradient(135deg, #e67e22 0%, #d35400 100%) !important; }
+.header-setting { background: linear-gradient(135deg, #8e44ad 0%, #732d91 100%) !important; }
+
+/* 主体容器 - 响应式 Flex 布局 */
.container {
display: flex;
- flex-direction: row; /* 横向排列 */
+ flex-wrap: wrap; /* 核心:允许在空间不足时换行 */
+ flex-direction: row;
justify-content: center;
align-items: center;
- gap: 20px; /* 适当缩小间距以适应屏幕宽度 */
- padding: 60px 20px;
+ gap: 20px;
+ padding: 40px 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+/* 详情页容器 */
+.sub-container {
+ padding: 30px 20px;
+ text-align: center;
+ max-width: 800px;
margin: 0 auto;
- overflow-x: auto; /* 如果屏幕太窄,允许横向滚动 */
}
/* 卡片按钮基础样式 */
.card {
- width: 260px; /* 略微调窄一点点确保一行能放下 */
- height: 80px;
+ flex: 1; /* 自动伸缩 */
+ min-width: 240px; /* 保证电脑端不会太窄 */
+ max-width: 280px; /* 保证电脑端不会太宽 */
+ height: 85px;
background-color: white;
- border-radius: 12px;
+ border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
text-decoration: none;
- box-shadow: 0 4px 6px rgba(0,0,0,0.05);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.05);
}
.card:hover {
transform: translateY(-5px);
- box-shadow: 0 8px 15px rgba(0,0,0,0.1);
+ box-shadow: 0 12px 20px rgba(0,0,0,0.15);
}
-/* 丰富的配色方案 */
-/* ① 科目面板 - 蓝色系 */
-.card-subject { color: #0066cc; border-left: 6px solid #0066cc; }
-.card-subject:hover { background-color: #f0f7ff; color: #004499; }
+/* 四个模块的丰富配色方案 */
+.card-subject { color: #0066cc; border-left: 8px solid #0066cc; }
+.card-subject:hover { background-color: #f0f7ff; }
-/* ② 题目管理 - 绿色系 */
-.card-question { color: #009944; border-left: 6px solid #009944; }
-.card-question:hover { background-color: #f0fff4; color: #007733; }
+.card-question { color: #009944; border-left: 8px solid #009944; }
+.card-question:hover { background-color: #f0fff4; }
-/* ③ 试卷管理 - 橙色系 */
-.card-paper { color: #e67e22; border-left: 6px solid #e67e22; }
-.card-paper:hover { background-color: #fffaf5; color: #d35400; }
+.card-paper { color: #e67e22; border-left: 8px solid #e67e22; }
+.card-paper:hover { background-color: #fffaf5; }
-/* ④ 系统设定 - 紫色/灰色系 */
-.card-setting { color: #8e44ad; border-left: 6px solid #8e44ad; }
-.card-setting:hover { background-color: #f9f5fb; color: #732d91; }
+.card-setting { color: #8e44ad; border-left: 8px solid #8e44ad; }
+.card-setting:hover { background-color: #f9f5fb; }
+/* 图标/数字样式 */
.icon-num {
margin-right: 12px;
font-size: 22px;
- font-style: italic;
- opacity: 0.8;
+ font-weight: bold;
+ opacity: 0.7;
}
-/* 子页面特定的 Header 颜色 */
-.header-subject { background: linear-gradient(135deg, #0066cc 0%, #004499 100%) !important; }
-.header-question { background: linear-gradient(135deg, #009944 0%, #007733 100%) !important; }
-.header-paper { background: linear-gradient(135deg, #e67e22 0%, #d35400 100%) !important; }
-.header-setting { background: linear-gradient(135deg, #8e44ad 0%, #732d91 100%) !important; }
+/* 返回按钮样式 */
+.back-btn {
+ display: inline-block;
+ margin-top: 30px;
+ padding: 12px 30px;
+ background: #666;
+ color: white;
+ text-decoration: none;
+ border-radius: 25px;
+ font-weight: 500;
+ transition: background 0.3s;
+}
+.back-btn:hover {
+ background: #333;
+}
+
+/* === 移动端适配 (iPhone 等) === */
+@media (max-width: 600px) {
+ .navbar {
+ font-size: 18px;
+ height: 50px;
+ }
+
+ .container {
+ padding: 20px 15px;
+ gap: 15px;
+ }
+ .card {
+ /* 在手机上,让按钮占据几乎全部宽度,防止并排重叠 */
+ min-width: 100%;
+ max-width: 100%;
+ height: 75px;
+ font-size: 17px;
+ border-left-width: 10px; /* 手机上加粗色块更醒目 */
+ }
+
+ .sub-container h2 {
+ font-size: 20px;
+ }
+}
========================================
>>> FILE END: static/css/style.css <<<
>>> FILE END: templates/questions/index.html <<<
+>>> FILE START: templates/questions/question_list.html <<<
+========================================
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>试题列表</title>
+ <link rel="stylesheet" href="/static/css/style.css">
+ <style>
+ body {
+ font-family: 'Microsoft YaHei', sans-serif;
+ background-color: #F0F0F0;
+ }
+ .header {
+ background-color: #4A90E2;
+ color: white;
+ padding: 15px;
+ text-align: center;
+ font-size: 24px;
+ }
+ .filter-panel {
+ margin: 20px;
+ background: white;
+ padding: 15px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ }
+ .filter-panel label {
+ margin-right: 10px;
+ }
+ .table-container {
+ margin: 20px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ }
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+ th, td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #ddd;
+ }
+ th {
+ background-color: #f2f2f2;
+ }
+ </style>
+</head>
+<body>
+ <div class="header">试题列表</div>
+ <div class="filter-panel">
+ <label for="subjectFilter">科目:</label>
+ <select id="subjectFilter">
+ <option value="全部">全部</option>
+ <!-- 动态加载科目选项 -->
+ {% for subject in subjects %}
+ <option value="{{ subject }}">{{ subject }}</option>
+ {% endfor %}
+ </select>
+
+ <label for="questionTypeFilter">题目类型:</label>
+ <select id="questionTypeFilter">
+ <option value="全部">全部</option>
+ <option value="错题">错题</option>
+ <option value="推题">推题</option>
+ </select>
+
+ <label for="answerStatusFilter">答题状态:</label>
+ <select id="answerStatusFilter">
+ <option value="全部">全部</option>
+ <option value="未完成">未完成</option>
+ <option value="正确">正确</option>
+ <option value="错误">错误</option>
+ <option value="部分正确">部分正确</option>
+ </select>
+
+ <button id="searchButton">搜索</button>
+ </div>
+
+ <div class="table-container">
+ <table id="questionTable">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>科目</th>
+ <th>题目类型</th>
+ <th>题干内容</th>
+ <th>答案解析</th>
+ <th>答题时间</th>
+ <th>输入时间</th>
+ <th>题目知识点</th>
+ </tr>
+ </thead>
+ <tbody id="questionBody">
+ <tr>
+ <td colspan="8">正在加载数据...</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <script>
+ async function loadQuestionData() {
+ const subject = document.getElementById('subjectFilter').value;
+ const questionType = document.getElementById('questionTypeFilter').value;
+ const answerStatus = document.getElementById('answerStatusFilter').value;
+
+ const response = await fetch(`/api/question_list?subject=${subject}&question_type=${questionType}&answer_status=${answerStatus}`);
+ const data = await response.json();
+
+ const questionBody = document.getElementById('questionBody');
+ questionBody.innerHTML = '';
+
+ if (data.error) {
+ questionBody.innerHTML = '<tr><td colspan="8">加载失败: ' + data.error + '</td></tr>';
+ return;
+ }
+
+ data.forEach(question => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ <td>${question.id}</td>
+ <td>${question.Subject || '-'}</td>
+ <td>${question.question_type || '-'}</td>
+ <td>${question.content || '-'}</td>
+ <td>${question.analysis || '-'}</td>
+ <td>${question.answer_time || '-'}</td>
+ <td>${question.input_time || '-'}</td>
+ <td>${question.knowledge_points || '-'}</td>
+ `;
+ questionBody.appendChild(row);
+ });
+ }
+
+ document.getElementById('searchButton').addEventListener('click', loadQuestionData);
+ document.addEventListener('DOMContentLoaded', loadQuestionData);
+ </script>
+</body>
+</html>
+========================================
+>>> FILE END: templates/questions/question_list.html <<<
+
+
>>> FILE START: templates/settings/index.html <<<
========================================
<!DOCTYPE html>
>>> FILE END: templates/settings/index.html <<<
+>>> FILE START: templates/subject/chapters.html <<<
+========================================
+<!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: 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="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 class="btn-query" onclick="loadData()">查询</button>
+ <a href="/subject/" class="btn-back-nav">返回</a>
+ </div>
+
+ <div class="table-container">
+ <table>
+ <thead>
+ <tr>
+ <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"></tbody>
+ </table>
+ </div>
+ </div>
+
+ <script>
+ let currentData = [];
+ let sortDirection = -1;
+
+ async function loadData() {
+ 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 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) {
+ 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>
+</html>
+========================================
+>>> FILE END: templates/subject/chapters.html <<<
+
+
>>> FILE START: templates/subject/index.html <<<
========================================
<!DOCTYPE html>
-<html lang="zh">
+<html lang="zh-CN">
<head>
<meta charset="UTF-8">
- <title>科目首页 - 错题系统</title>
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
+ <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>
- .sub-container { padding: 50px; text-align: center; }
- .back-btn { display: inline-block; margin-top: 20px; padding: 10px 20px; background: #666; color: white; text-decoration: none; border-radius: 5px; }
+ /* 针对统计页面的微调 */
+ .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; }
+
+ .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>
- <div class="navbar header-subject">科目首页</div>
+ <!-- 顶部导航 -->
+ <div class="navbar header-subject">
+ <span class="nav-title">科目概要面板</span>
+ </div>
+
<div class="sub-container">
- <h2>这里是科目首页内容区域</h2>
- <p>正在从原 Python 程序迁移逻辑...</p>
- <a href="/" class="back-btn">返回首页</a>
+ <!-- 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>
+
+ <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">
+ <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 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 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 stages = getSelectedStages().join(',');
+
+ if (!stages) {
+ updateUI(null);
+ return;
+ }
+
+ 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) {
+ 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('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>
</html>
========================================
>>> FILE END: templates/subject/index.html <<<
+
+>>> FILE START: templates/subject/knowledge.html <<<
+========================================
+<!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: 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-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 class="btn-query" onclick="loadData()">查询</button>
+ <a href="/subject/chapters" class="btn-back-nav">返回</a>
+ </div>
+
+ <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>
+ 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 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 || 0}</td>
+ <td><span class="num-badge">${row.not_correct || 0}</span></td>
+ </tr>
+ `).join('');
+ }
+ window.onload = loadData;
+ </script>
+</body>
+</html>
+========================================
+>>> FILE END: templates/subject/knowledge.html <<<
+