From: root Date: Wed, 20 May 2026 13:51:54 +0000 (+0800) Subject: 添加了知识点首页和章节首页功能 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=52bc00c6dbfd65213e9b3f31d4d7feddb0094565;p=daydayup.git 添加了知识点首页和章节首页功能 --- diff --git a/studybase/backend/__pycache__/app.cpython-312.pyc b/studybase/backend/__pycache__/app.cpython-312.pyc index eeda479..d8b53fb 100644 Binary files a/studybase/backend/__pycache__/app.cpython-312.pyc and b/studybase/backend/__pycache__/app.cpython-312.pyc differ diff --git a/studybase/backend/app.py b/studybase/backend/app.py index 07e0968..c710806 100644 --- a/studybase/backend/app.py +++ b/studybase/backend/app.py @@ -1,42 +1,34 @@ from flask import Flask, render_template, request, jsonify +from flask_cors import CORS import os from database import query_db -# 创建 Flask 应用实例 app = Flask(__name__) +CORS(app) -# ==================== 1. 核心页面路由 ==================== - +# --- 1. 页面路由 --- @app.route('/') def index(): return render_template('index.html') -# --- 题目列表模块 --- -@app.route('/questions/') -def questions_list(): - return render_template('questions/index.html') - -# ==================== 2. API 数据接口 ==================== - +# --- 2. 统计接口 --- @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: + return jsonify({"error": "Missing subject_id"}), 400 - 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') + def get_count(q_type, extra_sql=""): + sql = f"SELECT COUNT(*) as c FROM questions WHERE subject_id = ? AND question_type = ? {extra_sql}" + res = query_db(sql, [sub_id, 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} @@ -44,35 +36,83 @@ def get_subject_stats(): except Exception as e: return jsonify({"error": str(e)}), 500 -# ==================== 3. 测试接口 (移动至此处) ==================== +# --- 3. 章节分析接口 --- +@app.route('/api/chapters') +def get_chapters(): + subject_id = request.args.get('subject_id') + sql = """ + SELECT + c.id as chapter_id, + c.chapter_desc as ChapterDesc, + s.Subject as SubjectName, + COUNT(q.id) as total, + SUM(CASE WHEN q.latest_status != '正确' OR q.latest_status IS NULL THEN 1 ELSE 0 END) as not_correct + FROM chapters c + JOIN Subject s ON c.subject_id = s.ID + LEFT JOIN knowledge_point kp ON kp.chapter_id = c.id + LEFT JOIN question_kp_relation qkr ON qkr.kp_id = kp.id + LEFT JOIN questions q ON q.id = qkr.question_id + WHERE 1=1 + """ + params = [] + if subject_id: + sql += " AND c.subject_id = ?" + params.append(subject_id) + sql += " GROUP BY c.id" + + try: + results = query_db(sql, params) + return jsonify([dict(row) for row in results]) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# --- 4. 知识点分析接口 --- +@app.route('/api/knowledge') +@app.route('/api/knowledge') +def get_knowledge(): + chapter_id = request.args.get('chapter_id') + subject_id = request.args.get('subject_id') + + sql = """ + SELECT + kp.id, + kp.content, + c.chapter_desc as chapter_name, + s.Subject as subject_name, + COUNT(qkr.question_id) as total, + SUM(CASE WHEN q.latest_status != '正确' OR q.latest_status IS NULL THEN 1 ELSE 0 END) as not_correct + FROM knowledge_point kp + JOIN Subject s ON kp.subject_id = s.ID + LEFT JOIN chapters c ON kp.chapter_id = c.id + LEFT JOIN question_kp_relation qkr ON qkr.kp_id = kp.id + LEFT JOIN questions q ON q.id = qkr.question_id + WHERE 1=1 + """ + params = [] + if chapter_id: + sql += " AND kp.chapter_id = ?" + params.append(chapter_id) + elif subject_id: + sql += " AND kp.subject_id = ?" + params.append(subject_id) + + sql += " GROUP BY kp.id" + + try: + results = query_db(sql, params) + return jsonify([dict(row) for row in results]) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# --- 5. 调试接口 --- @app.route('/debug/db_test') def db_test(): try: - # 1. 检查数据库文件路径 - from database import DB_PATH - db_exists = os.path.exists(DB_PATH) - - # 2. 测试基础连接:查询科目表 - subjects = query_db("SELECT * FROM Subject LIMIT 5") - - # 3. 测试 questions 表:查一下总题数 - q_count = query_db("SELECT COUNT(*) as total FROM questions", one=True) - - return jsonify({ - "status": "success", - "database_path": DB_PATH, - "db_exists": db_exists, - "subjects_found": [dict(s) for s in subjects], - "total_questions": q_count['total'] if q_count else 0 - }) + subjects = query_db("SELECT * FROM Subject WHERE enabled = 1") + return jsonify({"status": "success", "subjects_found": [dict(s) for s in subjects]}) except Exception as e: - return jsonify({ - "status": "error", - "message": str(e) - }), 500 - -# ==================== 4. 启动服务 (必须在最后) ==================== + return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5002, debug=True) diff --git a/studybase/frontend/package.json b/studybase/frontend/package.json index a7d96fd..331740e 100644 --- a/studybase/frontend/package.json +++ b/studybase/frontend/package.json @@ -19,6 +19,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "proxy": "http://localhost:5002", "eslintConfig": { "extends": [ "react-app", diff --git a/studybase/frontend/src/App.css b/studybase/frontend/src/App.css index b85114e..b1de159 100644 --- a/studybase/frontend/src/App.css +++ b/studybase/frontend/src/App.css @@ -102,3 +102,25 @@ body { border-left-width: 10px; /* 手机上加粗色块更醒目 */ } } + +/* 辅助样式 */ +.sub-container { + padding: 20px; +} + +.back-btn:hover { + background-color: #444; + transition: 0.3s; +} + +.header-subject { + background: linear-gradient(135deg, #0066cc 0%, #004488 100%) !important; +} + +/* 让 Link 渲染的 a 标签也具备卡片样式 */ +.analysis-btn-group .card { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} diff --git a/studybase/frontend/src/App.js b/studybase/frontend/src/App.js index bac0155..4a8cb64 100644 --- a/studybase/frontend/src/App.js +++ b/studybase/frontend/src/App.js @@ -4,6 +4,8 @@ import './App.css'; // 导入页面组件 import Subject from './templates/subject/Subject'; +import Chapters from './templates/subject/Chapters'; // 新增 +import Knowledge from './templates/subject/Knowledge'; // 新增 import Questions from './templates/questions/Questions'; import Papers from './templates/papers/Papers'; import Settings from './templates/settings/Settings'; @@ -40,6 +42,8 @@ function App() { {/* 当 URL 匹配以下路径时,只渲染对应的组件,不再渲染 Home 组件的内容 */} } /> + } /> + } /> } /> } /> } /> diff --git a/studybase/frontend/src/templates/subject/Chapters.js b/studybase/frontend/src/templates/subject/Chapters.js new file mode 100644 index 0000000..84a3dec --- /dev/null +++ b/studybase/frontend/src/templates/subject/Chapters.js @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; + +function Chapters() { + const [searchParams] = useSearchParams(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [sortConfig, setSortConfig] = useState({ key: 'not_correct', direction: 'desc' }); + const subjectId = searchParams.get('subject_id'); + + useEffect(() => { + fetch(`/api/chapters?subject_id=${subjectId || ''}`) + .then(res => res.json()) + .then(json => { + // 默认按待完成数量逆序排列 + const sortedData = [...json].sort((a, b) => b.not_correct - a.not_correct); + setData(sortedData); + setLoading(false); + }) + .catch(err => { + console.error("加载章节失败:", err); + setLoading(false); + }); + }, [subjectId]); + + // 排序函数 + const requestSort = (key) => { + let direction = 'desc'; + if (sortConfig.key === key && sortConfig.direction === 'desc') { + direction = 'asc'; + } + setSortConfig({ key, direction }); + + const sortedData = [...data].sort((a, b) => { + if (a[key] < b[key]) return direction === 'asc' ? -1 : 1; + if (a[key] > b[key]) return direction === 'asc' ? 1 : -1; + return 0; + }); + setData(sortedData); + }; + + const getSortIcon = (key) => { + if (sortConfig.key !== key) return '↕️'; + return sortConfig.direction === 'asc' ? '🔼' : '🔽'; + }; + + return ( +
+
章节分析
+
+
+ 返回 +
+ +
+ + + + + + + + + + {loading ? ( + + ) : data.length === 0 ? ( + + ) : data.map((row, index) => ( + + + + + + ))} + +
requestSort('ChapterDesc')} style={{ padding: '12px', textAlign: 'left' }}>章节名称 {getSortIcon('ChapterDesc')} requestSort('total')} style={{ padding: '12px', textAlign: 'center' }}>总数 {getSortIcon('total')} requestSort('not_correct')} style={{ padding: '12px', textAlign: 'center' }}>待完成 {getSortIcon('not_correct')}
加载中...
暂无数据
+ + {row.ChapterDesc || '未命名章节'} + + {row.total} + {row.not_correct} +
+
+
+
+ ); +} + +export default Chapters; diff --git a/studybase/frontend/src/templates/subject/Knowledge.js b/studybase/frontend/src/templates/subject/Knowledge.js new file mode 100644 index 0000000..2844241 --- /dev/null +++ b/studybase/frontend/src/templates/subject/Knowledge.js @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; + +function Knowledge() { + const [searchParams] = useSearchParams(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [sortConfig, setSortConfig] = useState({ key: 'not_correct', direction: 'desc' }); + const chapterId = searchParams.get('chapter_id'); + const subjectId = searchParams.get('subject_id'); + + useEffect(() => { + let url = `/api/knowledge?`; + if (chapterId) url += `chapter_id=${chapterId}`; + else if (subjectId) url += `subject_id=${subjectId}`; + + fetch(url) + .then(res => res.json()) + .then(json => { + // 默认按待完成数量逆序排列 + const sortedData = [...json].sort((a, b) => b.not_correct - a.not_correct); + setData(sortedData); + setLoading(false); + }) + .catch(err => { + console.error("加载知识点失败:", err); + setLoading(false); + }); + }, [chapterId, subjectId]); + + const requestSort = (key) => { + let direction = 'desc'; + if (sortConfig.key === key && sortConfig.direction === 'desc') { + direction = 'asc'; + } + setSortConfig({ key, direction }); + + const sortedData = [...data].sort((a, b) => { + const valA = a[key] || ''; + const valB = b[key] || ''; + if (valA < valB) return direction === 'asc' ? -1 : 1; + if (valA > valB) return direction === 'asc' ? 1 : -1; + return 0; + }); + setData(sortedData); + }; + + const getSortIcon = (key) => { + if (sortConfig.key !== key) return '↕️'; + return sortConfig.direction === 'asc' ? '🔼' : '🔽'; + }; + + return ( +
+
知识点分析
+
+
+ +
+ +
+ + + + + + + + + + + {loading ? ( + + ) : data.length === 0 ? ( + + ) : data.map((row, index) => ( + + + + + + + ))} + +
requestSort('chapter_name')} style={{ padding: '12px', textAlign: 'left' }}>所属章节 {getSortIcon('chapter_name')} requestSort('content')} style={{ padding: '12px', textAlign: 'left' }}>知识点 {getSortIcon('content')} requestSort('total')} style={{ padding: '12px', textAlign: 'center' }}>总数 {getSortIcon('total')} requestSort('not_correct')} style={{ padding: '12px', textAlign: 'center' }}>待完成 {getSortIcon('not_correct')}
加载中...
暂无数据
{row.chapter_name || '-'}{row.content}{row.total} + {row.not_correct} +
+
+
+
+ ); +} + +export default Knowledge; diff --git a/studybase/frontend/src/templates/subject/Subject.js b/studybase/frontend/src/templates/subject/Subject.js index 39f315e..5211173 100644 --- a/studybase/frontend/src/templates/subject/Subject.js +++ b/studybase/frontend/src/templates/subject/Subject.js @@ -1,13 +1,101 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; function Subject() { + const [subjects, setSubjects] = useState([]); + const [selectedSubject, setSelectedSubject] = useState(''); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + // 1. 初始化:获取科目列表 + useEffect(() => { + fetch('/debug/db_test') + .then(res => res.json()) + .then(data => { + if (data.status === "success") { + setSubjects(data.subjects_found); + if (data.subjects_found.length > 0) { + setSelectedSubject(data.subjects_found[0].ID); + } + } + setLoading(false); + }) + .catch(err => console.error("加载科目失败:", err)); + }, []); + + // 2. 核心逻辑:当选中的科目 ID 改变时,获取统计数据 + useEffect(() => { + if (!selectedSubject) return; + + // 调用你后端 app.py 里的 /api/subject_stats 接口 + // 假设默认查询阶段 1 和 2 (stage_ids=1,2) + fetch(`/api/subject_stats?subject_id=${selectedSubject}&stage_ids=1,2`) + .then(res => res.json()) + .then(data => { + setStats(data); + }) + .catch(err => console.error("加载统计数据失败:", err)); + }, [selectedSubject]); + return (
-
科目面板
-
-

这里是科目面板内容区域

-

正在从原 Python 程序迁移逻辑...

- 返回首页 {/* 保留返回首页的按钮 */} +
+ 科目概要面板 +
+ +
+ + {/* 科目选择卡片 */} +
+ + +
+ + {/* 统计数据展示区域 (原本 HTML 中的概要信息) */} + {stats && ( +
+ {/* 错题统计 */} +
+

❌ 错题统计

+

总数:{stats.wrong.total}

+

已消灭:{stats.wrong.fixed}

+

剩余:{stats.wrong.remained}

+

消灭率:{stats.wrong.ratio}

+
+ {/* 推题统计 */} +
+

🚀 推题统计

+

总数:{stats.push.total}

+

已完成:{stats.push.fixed}

+

剩余:{stats.push.remained}

+

完成率:{stats.push.ratio}

+
+
+ )} + + {/* 功能导航按钮 */} +
+ + 📊 章节分析 + + + 💡 知识点分析 + +
+ +
+ + 返回首页 + +
); diff --git a/studybase/project_all_in_one.txt b/studybase/project_all_in_one.txt index 3c93b90..f9bcbef 100644 --- a/studybase/project_all_in_one.txt +++ b/studybase/project_all_in_one.txt @@ -5,6 +5,7 @@ .env .gitignore README.md + code-server_4.95.3_amd64.deb package-lock.json package.json backend/ @@ -69,6 +70,11 @@ index.html manifest.json robots.txt + s/ + schema.sql + studybase.db + studybase.sqbpro + workmode-studybase.sql ================================================== @@ -153,7 +159,84 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/t >>> FILE START: backend/app.py <<< ======================================== - +from flask import Flask, render_template, request, jsonify +import os +from database import query_db + +# 创建 Flask 应用实例 +app = Flask(__name__) + +# ==================== 1. 核心页面路由 ==================== + +@app.route('/') +def index(): + return render_template('index.html') + +# --- 题目列表模块 --- +@app.route('/questions/') +def questions_list(): + return render_template('questions/index.html') + +# ==================== 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 + +# ==================== 3. 测试接口 (移动至此处) ==================== + +@app.route('/debug/db_test') +def db_test(): + try: + # 1. 检查数据库文件路径 + from database import DB_PATH + db_exists = os.path.exists(DB_PATH) + + # 2. 测试基础连接:查询科目表 + subjects = query_db("SELECT * FROM Subject LIMIT 5") + + # 3. 测试 questions 表:查一下总题数 + q_count = query_db("SELECT COUNT(*) as total FROM questions", one=True) + + return jsonify({ + "status": "success", + "database_path": DB_PATH, + "db_exists": db_exists, + "subjects_found": [dict(s) for s in subjects], + "total_questions": q_count['total'] if q_count else 0 + }) + except Exception as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 500 + +# ==================== 4. 启动服务 (必须在最后) ==================== + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5002, debug=True) ======================================== >>> FILE END: backend/app.py <<< @@ -167,7 +250,34 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/t >>> FILE START: backend/database.py <<< ======================================== - +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: backend/database.py <<< @@ -650,7 +760,7 @@ function Papers() {
试卷管理
-

这里是试卷管理内容区域

+

这里是试卷管理内容区域11223344

正在从原 Python 程序迁移逻辑...

返回首页