From f2f12c908d851c6a6608a8125e3b110c4e77c93c Mon Sep 17 00:00:00 2001 From: gailgou <717389475@qq.com> Date: Mon, 27 Apr 2026 14:47:57 +0800 Subject: [PATCH] study1 --- .DS_Store | Bin 0 -> 6148 bytes __pycache__/config.cpython-314.pyc | Bin 0 -> 1071 bytes app/__init__.py | 36 ++ app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1854 bytes app/__pycache__/models.cpython-314.pyc | Bin 0 -> 9738 bytes app/models.py | 102 ++++ app/routes/__init__.py | 7 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 444 bytes app/routes/__pycache__/auth.cpython-314.pyc | Bin 0 -> 4341 bytes app/routes/__pycache__/goals.cpython-314.pyc | Bin 0 -> 7554 bytes app/routes/__pycache__/main.cpython-314.pyc | Bin 0 -> 3218 bytes .../__pycache__/reports.cpython-314.pyc | Bin 0 -> 10607 bytes app/routes/__pycache__/tasks.cpython-314.pyc | Bin 0 -> 15343 bytes app/routes/auth.py | 70 +++ app/routes/goals.py | 115 ++++ app/routes/main.py | 41 ++ app/routes/reports.py | 177 +++++++ app/routes/tasks.py | 251 +++++++++ app/templates/base.html | 493 ++++++++++++++++++ app/templates/goals/create.html | 27 + app/templates/goals/edit.html | 43 ++ app/templates/goals/list.html | 77 +++ app/templates/goals/view.html | 188 +++++++ app/templates/index.html | 238 +++++++++ app/templates/login.html | 29 ++ app/templates/register.html | 31 ++ app/templates/reports/generate.html | 54 ++ app/templates/reports/history.html | 110 ++++ app/templates/reports/list.html | 56 ++ app/templates/reports/view.html | 92 ++++ app/templates/tasks/create.html | 48 ++ app/templates/tasks/edit.html | 51 ++ app/templates/tasks/view.html | 361 +++++++++++++ config.py | 9 + requirements.txt | 7 + run.py | 6 + study_plan.db | Bin 0 -> 36864 bytes 37 files changed, 2719 insertions(+) create mode 100644 .DS_Store create mode 100644 __pycache__/config.cpython-314.pyc create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-314.pyc create mode 100644 app/__pycache__/models.cpython-314.pyc create mode 100644 app/models.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/__pycache__/__init__.cpython-314.pyc create mode 100644 app/routes/__pycache__/auth.cpython-314.pyc create mode 100644 app/routes/__pycache__/goals.cpython-314.pyc create mode 100644 app/routes/__pycache__/main.cpython-314.pyc create mode 100644 app/routes/__pycache__/reports.cpython-314.pyc create mode 100644 app/routes/__pycache__/tasks.cpython-314.pyc create mode 100644 app/routes/auth.py create mode 100644 app/routes/goals.py create mode 100644 app/routes/main.py create mode 100644 app/routes/reports.py create mode 100644 app/routes/tasks.py create mode 100644 app/templates/base.html create mode 100644 app/templates/goals/create.html create mode 100644 app/templates/goals/edit.html create mode 100644 app/templates/goals/list.html create mode 100644 app/templates/goals/view.html create mode 100644 app/templates/index.html create mode 100644 app/templates/login.html create mode 100644 app/templates/register.html create mode 100644 app/templates/reports/generate.html create mode 100644 app/templates/reports/history.html create mode 100644 app/templates/reports/list.html create mode 100644 app/templates/reports/view.html create mode 100644 app/templates/tasks/create.html create mode 100644 app/templates/tasks/edit.html create mode 100644 app/templates/tasks/view.html create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 study_plan.db diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0uKpD%bcJ-(3?HP`nOG;b4}rTt>=_24 zE!e4tdqwUZ80UYmz(kIrC06xzi{4adWz2~H_iWg)EyE|x9<9*JGwJ@*W|qiV)t_J{iS&`) zu%>P*1>#Ia!(udzmZR$jUIUl&`+-tXQUivZ(`6jkmiGfQQ!_#0iiFak=b=;-t%)lK zPGh&IV0jioc;Gg(5X+j(XBR($#CSh(Af!q=GD5_O(2kZb7n$xtXtyjCJDvfAHWl5# zMMafWjkxZ4TN;|jkVdp$Rb=bWZi%T zt-Pa1+ODFh1sIWyZLF1GSj)>W6hq6ny>0RaWy@Gub%iP`b@_wFVB2;8e8?PhFVuoH=4e=3&4#ov>2Q@ zZoSuQd)2+#VfFCj#VKkI&wp{uw`{;Za{l1aXMRi{vTBVBx{K)p6o?>Y4Rdw^ofsQB zTg#@xR-t>W472k~g4-;~F?YGH1>H+c_LCXY6k0Zp%-5YbOxpqg7WxN=}&-y>?2isTGZY%ukrLCB0H+f&(at!pp+DwE&*i(Oi3NE{p_?1v-nF zWo97WonwaXv181(twb~z3m0b+A=ja^Ot?QFlcE0qka8Ec);?cmIy=fS;tF)_aTb@Qke20dXB9fEz8KYGL zgZi@%?M9>MKuduta5KY5hua>3Rr(wSUpRZv~$Ox~cS>FxoOJ70X&eB-S@ zKmMlq?T5|F?=|0i_s;e2Hm_X&yZ?1xk6#o6?uTu19d=7ZSVQhj0#M=wmCpS*>$KjQ31M;rK& z-!XsTXY}P!2cZ22I4<9&HMM`_ZLREA4U>|e0(~W@VF)VJ80E)pU*4yIYVvz7YO}=Lvd4x z;ukLAKtnQg;_1{+r(JZSw|Nz9!HmemFnO_~Rddlbv~h9}MEYN_Y1iLn8HU+FgB$3; j26|`%J@_k^d2Oy9`K538{d7HbdwB2PXo$%W`dRr8fLpR3 literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-314.pyc b/app/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5107205e4ea2e7d4d6c361831c5808f2553efee GIT binary patch literal 9738 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 RWa@fi;mn)9#2oAHe*ru6%' + +class LearningGoal(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + start_date = db.Column(db.DateTime, default=datetime.utcnow) + end_date = db.Column(db.DateTime) + status = db.Column(db.String(20), default='active') + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + tasks = db.relationship('LearningTask', backref='goal', lazy='dynamic', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + +class LearningTask(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + goal_id = db.Column(db.Integer, db.ForeignKey('learning_goal.id'), nullable=False) + parent_task_id = db.Column(db.Integer, db.ForeignKey('learning_task.id')) + status = db.Column(db.String(20), default='pending') + progress = db.Column(db.Integer, default=0) + priority = db.Column(db.String(20), default='medium') + estimated_hours = db.Column(db.Float, default=0) + actual_hours = db.Column(db.Float, default=0) + deadline = db.Column(db.DateTime) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + completed_at = db.Column(db.DateTime) + + parent_task = db.relationship('LearningTask', remote_side=[id], backref='subtasks') + study_records = db.relationship('StudyRecord', backref='task', lazy='dynamic', cascade='all, delete-orphan') + reminders = db.relationship('TaskReminder', backref='task', lazy='dynamic', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + +class StudyRecord(db.Model): + id = db.Column(db.Integer, primary_key=True) + task_id = db.Column(db.Integer, db.ForeignKey('learning_task.id'), nullable=False) + start_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + end_time = db.Column(db.DateTime) + duration_minutes = db.Column(db.Integer, default=0) + notes = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' + +class TaskReminder(db.Model): + id = db.Column(db.Integer, primary_key=True) + task_id = db.Column(db.Integer, db.ForeignKey('learning_task.id'), nullable=False) + reminder_time = db.Column(db.DateTime, nullable=False) + message = db.Column(db.String(200)) + is_sent = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' + +class StudyReport(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + report_type = db.Column(db.String(20), nullable=False) + start_date = db.Column(db.DateTime, nullable=False) + end_date = db.Column(db.DateTime, nullable=False) + total_study_hours = db.Column(db.Float, default=0) + completed_tasks = db.Column(db.Integer, default=0) + total_tasks = db.Column(db.Integer, default=0) + goals_progress = db.Column(db.Text) + recommendations = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..04d95f3 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +main = Blueprint('main', __name__) +auth = Blueprint('auth', __name__) +goals = Blueprint('goals', __name__) +tasks = Blueprint('tasks', __name__) +reports = Blueprint('reports', __name__) diff --git a/app/routes/__pycache__/__init__.cpython-314.pyc b/app/routes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4a248676603a763972612a65ca17241a0e97422 GIT binary patch literal 444 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 literal 0 HcmV?d00001 diff --git a/app/routes/__pycache__/goals.cpython-314.pyc b/app/routes/__pycache__/goals.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ae45e9227529cd038f6ee73e109c40da2c0af58 GIT binary patch literal 7554 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 QHa2ZxQ}9<0;Ora!1dUu&8vp-! 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 literal 0 HcmV?d00001 diff --git a/app/routes/__pycache__/tasks.cpython-314.pyc b/app/routes/__pycache__/tasks.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1478d11c6cf58acfe752bb87a9bfb5c26e73c534 GIT binary patch literal 15343 zcmeHOdvF`ac|QP$2LX@(NxVpqA|yUch!2<+W$Ho6R`nt!Qie}79LJDoT7W1-AORl$ z+ftN-O4^9ZxRI17krX>4I^#5SXKb?`x0Yi$Ry*m#oy-)_!2`?qmsDH#8F{$$x zH{ViH)D*R!VibeafFiCKP{x%5s<>)^j?)9`xOzYn*EHdC<)C&z7uO9`#4E_XYEVC5 zh#SZ?J!l-Lj8~Ft^sxNCZ!J^bxnzCYZT0PC>NhM|-*H?0x-#_}m#klZ zTYYDl`YV`*X3E`o13$A{!<*RPWFW|n#)A{l@K`*^>)2o*#0C$FI(Wpbw2h1Ldz2pDvP_{R}QlgrBsIT8o`Ca1Z7*!vxMMzQYJ+x(zMxwii+T~Y6 zg^w0j!uYC^RVrzSC01$W<4UGV9Ff?!W(wAq-lr2=4b)+)PRb9h3&{&QwluAzd^*M? z&9vU{6eWDLPwk_86+Vqm>(evUeOlp3)ZlLyWydHI0MjZDNb5!dyltTr^BJM}AKJ4%w@lA~N@mj_v&TKQ~qWo>*GYu7J#ThN?g;{K6 ziC$DbQRYh}l#Ns~6{0q(8mW-NZBO8iS~xee@cpmNU;6RFYp;Cr;_C}DC$3Gucy2;RfxbV`& zg_+COE}yz__QLfy-uUI^7ohF=_ul{H?CcYex`v0lwrvw6+*%gbAd7&;uEY$RWE(MS z!psF(0!6O9_9yf2%m{rGCTn5(^tEqX{N?52pPGbDJ{t-?N4f~pqs`A$egJvJ)_o{G z5rzTKH~`*IFsbf37MB~Z293-f;q~Le_-KS3?Y+Mj)HbMIm~)RNFeChGB~>Rk!{;O z_xC=~=ZPV|@a##1!%Q$5@r*^I9yXGQ2V)-Ldqj`$hJuB|_QU&7i^qNm++m8krJ;Ua?Ch~~Upe!Yw6-m$t4vmPr*)p3$^Mcy>3V>3 ztl><(NnP)}wc(uqj6Y*-O<7x$Z4X}Fo3yra)-6+evNg7;-5+VKIoqm?tvzLH&)7Os zw$9o8oUMP_Ft4pT@#W)RK5zR_+mtig&l%4cGiF!H?3%H2X7{u@XKOqcJ`>K^+ETW* znJ;m+wbO=NZT-3aGyR#`=2UI-%&s{dSG#^%m#bfSvGYP_rhav*es!{=|I*k`gUR~U zT>a3rDQ91C?%0`Q8T)-H`+c)|&c0^an6o&~4V@XvSXxq+mSpSN%UhF{7S8hUbj2qo zOU_uGtZ}D}t8=x^mv$yw`mWg14TreeP|_IsxYjex7?S^}gTtPG8!wl{0Ni>bA`r>XXjyxskMCZ<5~o zn_Kr$mKC=sC3eSbP1<&(%{z1UrdPDd_KjB_PrJgLed4NlVo{9}pT>}o{LNhJXm$d6VDTr@nLz6jEG#4d|>tVp zE?AbmmhZ7@*&fR+x=LOPxBulHs|wgqIXmQDdb}*Zx}$ld{UT`t4rix)DzoHA5bH

uLQH?`QBu@LO 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 z W9Qw<+cLmV~q5!L>&LUY3TRcErYWi~N)a5j=wY|bi7|E%m*j3{oZD5V**hZbqP zUwleIDYb%lBd1tF1Zxy)B)O(V8g3WcC8<@jkIn6veI%{uC$cLP>m}Jui!|IWJ}fuc zGW+c8p|qlpi0Tzv') +@login_required +def view_goal(goal_id): + goal = LearningGoal.query.get_or_404(goal_id) + if goal.user_id != current_user.id: + flash('无权访问此目标') + return redirect(url_for('goals.list_goals')) + + tasks = LearningTask.query.filter_by(goal_id=goal_id, parent_task_id=None).order_by(LearningTask.created_at.desc()).all() + + total_tasks = LearningTask.query.filter_by(goal_id=goal_id).count() + completed_tasks = LearningTask.query.filter_by(goal_id=goal_id, status='completed').count() + in_progress_tasks = LearningTask.query.filter_by(goal_id=goal_id, status='in_progress').count() + + stats = { + 'total': total_tasks, + 'completed': completed_tasks, + 'in_progress': in_progress_tasks, + 'pending': total_tasks - completed_tasks - in_progress_tasks, + 'completion_rate': round((completed_tasks / total_tasks * 100), 1) if total_tasks > 0 else 0 + } + + return render_template('goals/view.html', goal=goal, tasks=tasks, stats=stats) + +@goals.route('//edit', methods=['GET', 'POST']) +@login_required +def edit_goal(goal_id): + goal = LearningGoal.query.get_or_404(goal_id) + if goal.user_id != current_user.id: + flash('无权编辑此目标') + return redirect(url_for('goals.list_goals')) + + if request.method == 'POST': + goal.title = request.form.get('title') + goal.description = request.form.get('description') + goal.status = request.form.get('status', 'active') + + end_date_str = request.form.get('end_date') + if end_date_str: + try: + goal.end_date = datetime.strptime(end_date_str, '%Y-%m-%d') + except ValueError: + flash('日期格式错误,请使用YYYY-MM-DD格式') + return redirect(url_for('goals.edit_goal', goal_id=goal.id)) + + db.session.commit() + flash('学习目标更新成功!') + return redirect(url_for('goals.view_goal', goal_id=goal.id)) + + return render_template('goals/edit.html', goal=goal) + +@goals.route('//delete', methods=['POST']) +@login_required +def delete_goal(goal_id): + goal = LearningGoal.query.get_or_404(goal_id) + if goal.user_id != current_user.id: + flash('无权删除此目标') + return redirect(url_for('goals.list_goals')) + + db.session.delete(goal) + db.session.commit() + flash('学习目标已删除') + return redirect(url_for('goals.list_goals')) diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..d183472 --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,41 @@ +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from app import db +from app.routes import main +from app.models import LearningGoal, LearningTask, StudyRecord +from datetime import datetime, timedelta + +@main.route('/') +@main.route('/index') +@login_required +def index(): + today = datetime.utcnow().date() + start_of_day = datetime.combine(today, datetime.min.time()) + + today_records = StudyRecord.query.join(LearningTask).join(LearningGoal).filter( + LearningGoal.user_id == current_user.id, + StudyRecord.start_time >= start_of_day + ).all() + + today_hours = sum((r.duration_minutes or 0) for r in today_records) / 60 + + active_goals = LearningGoal.query.filter_by( + user_id=current_user.id, + status='active' + ).all() + + pending_tasks = LearningTask.query.join(LearningGoal).filter( + LearningGoal.user_id == current_user.id, + LearningTask.status == 'pending' + ).order_by(LearningTask.deadline).limit(5).all() + + in_progress_tasks = LearningTask.query.join(LearningGoal).filter( + LearningGoal.user_id == current_user.id, + LearningTask.status == 'in_progress' + ).order_by(LearningTask.deadline).limit(5).all() + + return render_template('index.html', + today_hours=round(today_hours, 2), + active_goals_count=len(active_goals), + pending_tasks=pending_tasks, + in_progress_tasks=in_progress_tasks) diff --git a/app/routes/reports.py b/app/routes/reports.py new file mode 100644 index 0000000..6ee58ba --- /dev/null +++ b/app/routes/reports.py @@ -0,0 +1,177 @@ +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from app import db +from app.routes import reports +from app.models import LearningGoal, LearningTask, StudyRecord, StudyReport +from datetime import datetime, timedelta +from collections import defaultdict + +@reports.route('/') +@login_required +def list_reports(): + reports_list = StudyReport.query.filter_by( + user_id=current_user.id + ).order_by(StudyReport.created_at.desc()).all() + return render_template('reports/list.html', reports=reports_list) + +@reports.route('/generate', methods=['GET', 'POST']) +@login_required +def generate_report(): + if request.method == 'POST': + report_type = request.form.get('report_type', 'weekly') + custom_start = request.form.get('start_date') + custom_end = request.form.get('end_date') + + end_date = datetime.utcnow() + + if report_type == 'weekly': + start_date = end_date - timedelta(weeks=1) + elif report_type == 'monthly': + start_date = end_date - timedelta(days=30) + elif report_type == 'custom' and custom_start and custom_end: + try: + start_date = datetime.strptime(custom_start, '%Y-%m-%d') + end_date = datetime.strptime(custom_end, '%Y-%m-%d') + except ValueError: + flash('日期格式错误,请使用YYYY-MM-DD格式') + return redirect(url_for('reports.generate_report')) + else: + start_date = end_date - timedelta(weeks=1) + + report = create_study_report(current_user.id, start_date, end_date, report_type) + + flash('学习报告生成成功!') + return redirect(url_for('reports.view_report', report_id=report.id)) + + return render_template('reports/generate.html') + +@reports.route('/') +@login_required +def view_report(report_id): + report = StudyReport.query.get_or_404(report_id) + if report.user_id != current_user.id: + flash('无权访问此报告') + return redirect(url_for('reports.list_reports')) + + return render_template('reports/view.html', report=report) + +@reports.route('/history') +@login_required +def view_history(): + page = request.args.get('page', 1, type=int) + per_page = 20 + + goals = LearningGoal.query.filter_by( + user_id=current_user.id + ).order_by(LearningGoal.created_at.desc()).paginate(page=page, per_page=per_page, error_out=False) + + completed_goals = LearningGoal.query.filter_by( + user_id=current_user.id, + status='completed' + ).count() + + all_goals = LearningGoal.query.filter_by( + user_id=current_user.id + ).count() + + total_study_hours = db.session.query( + db.func.sum(StudyRecord.duration_minutes) + ).join(LearningTask).join(LearningGoal).filter( + LearningGoal.user_id == current_user.id + ).scalar() or 0 + + stats = { + 'total_goals': all_goals, + 'completed_goals': completed_goals, + 'completion_rate': round((completed_goals / all_goals * 100), 1) if all_goals > 0 else 0, + 'total_study_hours': round(total_study_hours / 60, 2) + } + + return render_template('reports/history.html', goals=goals, stats=stats) + +def create_study_report(user_id, start_date, end_date, report_type): + study_records = StudyRecord.query.join(LearningTask).join(LearningGoal).filter( + LearningGoal.user_id == user_id, + StudyRecord.start_time >= start_date, + StudyRecord.start_time <= end_date + ).all() + + total_minutes = sum((r.duration_minutes or 0) for r in study_records) + total_hours = round(total_minutes / 60, 2) + + tasks = LearningTask.query.join(LearningGoal).filter( + LearningGoal.user_id == user_id + ).all() + + total_tasks = len(tasks) + completed_tasks = len([t for t in tasks if t.status == 'completed']) + + daily_stats = defaultdict(int) + for record in study_records: + if record.start_time: + date_key = record.start_time.date().isoformat() + daily_stats[date_key] += (record.duration_minutes or 0) + + goals_progress = [] + goals = LearningGoal.query.filter_by(user_id=user_id, status='active').all() + for goal in goals: + goal_tasks = LearningTask.query.filter_by(goal_id=goal.id).all() + total_goal_tasks = len(goal_tasks) + completed_goal_tasks = len([t for t in goal_tasks if t.status == 'completed']) + progress = round((completed_goal_tasks / total_goal_tasks * 100), 1) if total_goal_tasks > 0 else 0 + + goals_progress.append({ + 'goal_title': goal.title, + 'total_tasks': total_goal_tasks, + 'completed_tasks': completed_goal_tasks, + 'progress': progress + }) + + recommendations = generate_recommendations(total_hours, completed_tasks, total_tasks, goals_progress) + + report = StudyReport( + user_id=user_id, + report_type=report_type, + start_date=start_date, + end_date=end_date, + total_study_hours=total_hours, + completed_tasks=completed_tasks, + total_tasks=total_tasks, + goals_progress=str(goals_progress), + recommendations=recommendations + ) + + db.session.add(report) + db.session.commit() + + return report + +def generate_recommendations(total_hours, completed_tasks, total_tasks, goals_progress): + recommendations = [] + + if total_hours < 5: + recommendations.append("本周学习时间较少,建议每天安排至少1-2小时的固定学习时间。") + elif total_hours < 10: + recommendations.append("学习时间尚可,继续保持,可以尝试增加学习深度。") + else: + recommendations.append("本周学习时间充足,继续保持良好的学习习惯!") + + if total_tasks > 0: + completion_rate = completed_tasks / total_tasks + if completion_rate < 0.3: + recommendations.append("任务完成率较低,建议先专注于完成高优先级任务,或减少每周任务量。") + elif completion_rate < 0.6: + recommendations.append("任务完成率一般,可以尝试将大任务拆分成更小的子任务。") + else: + recommendations.append("任务完成率不错,继续保持高效的执行力!") + + for goal in goals_progress: + if goal['progress'] < 30: + recommendations.append(f"目标「{goal['goal_title']}」进度较慢,建议增加对此目标的关注。") + elif goal['progress'] >= 80: + recommendations.append(f"目标「{goal['goal_title']}」进展良好,继续保持!") + + if not recommendations: + recommendations.append("继续保持当前的学习节奏,定期回顾和调整学习计划。") + + return "\n\n".join(recommendations) diff --git a/app/routes/tasks.py b/app/routes/tasks.py new file mode 100644 index 0000000..006b3e2 --- /dev/null +++ b/app/routes/tasks.py @@ -0,0 +1,251 @@ +from flask import render_template, redirect, url_for, flash, request, jsonify +from flask_login import login_required, current_user +from app import db +from app.routes import tasks +from app.models import LearningGoal, LearningTask, StudyRecord, TaskReminder +from datetime import datetime, timedelta + +@tasks.route('/create/', methods=['GET', 'POST']) +@login_required +def create_task(goal_id): + goal = LearningGoal.query.get_or_404(goal_id) + if goal.user_id != current_user.id: + flash('无权为此目标创建任务') + return redirect(url_for('goals.list_goals')) + + parent_task_id = request.args.get('parent_task_id', type=int) + parent_task = None + if parent_task_id: + parent_task = LearningTask.query.get(parent_task_id) + + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + priority = request.form.get('priority', 'medium') + estimated_hours = request.form.get('estimated_hours', 0, type=float) + deadline_str = request.form.get('deadline') + parent_id = request.form.get('parent_task_id', type=int) + + if not title: + flash('请输入任务标题') + return redirect(url_for('tasks.create_task', goal_id=goal_id, parent_task_id=parent_task_id)) + + deadline = None + if deadline_str: + try: + deadline = datetime.strptime(deadline_str, '%Y-%m-%d') + except ValueError: + flash('日期格式错误,请使用YYYY-MM-DD格式') + return redirect(url_for('tasks.create_task', goal_id=goal_id, parent_task_id=parent_task_id)) + + task = LearningTask( + title=title, + description=description, + goal_id=goal_id, + parent_task_id=parent_id, + priority=priority, + estimated_hours=estimated_hours, + deadline=deadline + ) + db.session.add(task) + db.session.commit() + + flash('学习任务创建成功!') + return redirect(url_for('tasks.view_task', task_id=task.id)) + + return render_template('tasks/create.html', goal=goal, parent_task=parent_task) + +@tasks.route('/') +@login_required +def view_task(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + flash('无权访问此任务') + return redirect(url_for('goals.list_goals')) + + subtasks = LearningTask.query.filter_by(parent_task_id=task_id).order_by(LearningTask.created_at.desc()).all() + study_records = StudyRecord.query.filter_by(task_id=task_id).order_by(StudyRecord.start_time.desc()).all() + reminders = TaskReminder.query.filter_by(task_id=task_id).order_by(TaskReminder.reminder_time).all() + + total_duration = sum((r.duration_minutes or 0) for r in study_records) + total_hours = round(total_duration / 60, 2) + + return render_template('tasks/view.html', + task=task, + subtasks=subtasks, + study_records=study_records, + reminders=reminders, + total_hours=total_hours) + +@tasks.route('//edit', methods=['GET', 'POST']) +@login_required +def edit_task(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + flash('无权编辑此任务') + return redirect(url_for('goals.list_goals')) + + if request.method == 'POST': + task.title = request.form.get('title') + task.description = request.form.get('description') + task.status = request.form.get('status', 'pending') + task.progress = request.form.get('progress', 0, type=int) + task.priority = request.form.get('priority', 'medium') + task.estimated_hours = request.form.get('estimated_hours', 0, type=float) + + deadline_str = request.form.get('deadline') + if deadline_str: + try: + task.deadline = datetime.strptime(deadline_str, '%Y-%m-%d') + except ValueError: + flash('日期格式错误,请使用YYYY-MM-DD格式') + return redirect(url_for('tasks.edit_task', task_id=task.id)) + + if task.status == 'completed' and task.progress < 100: + task.progress = 100 + task.completed_at = datetime.utcnow() + + db.session.commit() + flash('学习任务更新成功!') + return redirect(url_for('tasks.view_task', task_id=task.id)) + + return render_template('tasks/edit.html', task=task) + +@tasks.route('//delete', methods=['POST']) +@login_required +def delete_task(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + flash('无权删除此任务') + return redirect(url_for('goals.list_goals')) + + goal_id = task.goal_id + db.session.delete(task) + db.session.commit() + flash('学习任务已删除') + return redirect(url_for('goals.view_goal', goal_id=goal_id)) + +@tasks.route('//progress', methods=['POST']) +@login_required +def update_progress(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + return jsonify({'success': False, 'message': '无权操作此任务'}), 403 + + progress = request.form.get('progress', type=int) + if progress is not None: + task.progress = min(100, max(0, progress)) + if task.progress >= 100: + task.status = 'completed' + task.completed_at = datetime.utcnow() + elif task.progress > 0: + task.status = 'in_progress' + + db.session.commit() + return jsonify({'success': True, 'progress': task.progress, 'status': task.status}) + + return jsonify({'success': False, 'message': '参数错误'}), 400 + +@tasks.route('//start-study', methods=['POST']) +@login_required +def start_study(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + return jsonify({'success': False, 'message': '无权操作此任务'}), 403 + + active_record = StudyRecord.query.filter_by( + task_id=task_id, + end_time=None + ).first() + + if active_record: + return jsonify({'success': False, 'message': '已有进行中的学习记录'}), 400 + + study_record = StudyRecord( + task_id=task_id, + start_time=datetime.utcnow() + ) + db.session.add(study_record) + + if task.status == 'pending': + task.status = 'in_progress' + + db.session.commit() + + return jsonify({ + 'success': True, + 'record_id': study_record.id, + 'start_time': study_record.start_time.isoformat() + }) + +@tasks.route('//stop-study', methods=['POST']) +@login_required +def stop_study(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + return jsonify({'success': False, 'message': '无权操作此任务'}), 403 + + active_record = StudyRecord.query.filter_by( + task_id=task_id, + end_time=None + ).first() + + if not active_record: + return jsonify({'success': False, 'message': '没有进行中的学习记录'}), 400 + + active_record.end_time = datetime.utcnow() + duration = (active_record.end_time - active_record.start_time).total_seconds() + active_record.duration_minutes = int(duration / 60) + + task.actual_hours += active_record.duration_minutes / 60 + + db.session.commit() + + return jsonify({ + 'success': True, + 'duration_minutes': active_record.duration_minutes, + 'total_hours': round(task.actual_hours, 2) + }) + +@tasks.route('//add-reminder', methods=['POST']) +@login_required +def add_reminder(task_id): + task = LearningTask.query.get_or_404(task_id) + if task.goal.user_id != current_user.id: + flash('无权为此任务添加提醒') + return redirect(url_for('tasks.view_task', task_id=task_id)) + + reminder_time_str = request.form.get('reminder_time') + message = request.form.get('message', '') + + try: + reminder_time = datetime.strptime(reminder_time_str, '%Y-%m-%d %H:%M') + except ValueError: + flash('提醒时间格式错误,请使用YYYY-MM-DD HH:MM格式') + return redirect(url_for('tasks.view_task', task_id=task_id)) + + reminder = TaskReminder( + task_id=task_id, + reminder_time=reminder_time, + message=message + ) + db.session.add(reminder) + db.session.commit() + + flash('提醒添加成功!') + return redirect(url_for('tasks.view_task', task_id=task_id)) + +@tasks.route('/reminder//delete', methods=['POST']) +@login_required +def delete_reminder(reminder_id): + reminder = TaskReminder.query.get_or_404(reminder_id) + if reminder.task.goal.user_id != current_user.id: + flash('无权删除此提醒') + return redirect(url_for('goals.list_goals')) + + task_id = reminder.task_id + db.session.delete(reminder) + db.session.commit() + + flash('提醒已删除') + return redirect(url_for('tasks.view_task', task_id=task_id)) diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..2490296 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,493 @@ + + + + + + {% block title %}个人学习计划管理系统{% endblock %} + + + +

+
+

📚 个人学习计划管理系统

+ +
+
+ +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + {% block scripts %}{% endblock %} + + diff --git a/app/templates/goals/create.html b/app/templates/goals/create.html new file mode 100644 index 0000000..812615a --- /dev/null +++ b/app/templates/goals/create.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}创建学习目标 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

创建学习目标

+
+
+ + +
+
+ + +
+
+ + +
+
+ + 取消 +
+
+
+{% endblock %} diff --git a/app/templates/goals/edit.html b/app/templates/goals/edit.html new file mode 100644 index 0000000..3578ba7 --- /dev/null +++ b/app/templates/goals/edit.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}编辑学习目标 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

编辑学习目标

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + 取消 +
+
+
+ +
+

危险操作

+

删除此目标将同时删除所有关联的任务和学习记录,此操作不可撤销。

+
+ +
+
+{% endblock %} diff --git a/app/templates/goals/list.html b/app/templates/goals/list.html new file mode 100644 index 0000000..906d90f --- /dev/null +++ b/app/templates/goals/list.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block title %}学习目标 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+

学习目标列表

+ 创建新目标 +
+ +
+ +
+ + {% if goals %} +
+ + + + + + + + + + + + + {% for goal in goals %} + + + + + + + + + {% endfor %} + +
目标名称创建日期截止日期状态任务数操作
+ + {{ goal.title }} + + {% if goal.description %} +
+ {{ goal.description[:50] }}{% if goal.description|length > 50 %}...{% endif %} + {% endif %} +
{{ goal.created_at.strftime('%Y-%m-%d') if goal.created_at else '-' }}{{ goal.end_date.strftime('%Y-%m-%d') if goal.end_date else '-' }} + {% if goal.status == 'active' %} + 进行中 + {% elif goal.status == 'completed' %} + 已完成 + {% elif goal.status == 'paused' %} + 已暂停 + {% else %} + {{ goal.status }} + {% endif %} + {{ goal.tasks.count() }} +
+ 查看 + 编辑 +
+
+
+ {% else %} +
+

还没有任何学习目标

+ 创建第一个学习目标 +
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/goals/view.html b/app/templates/goals/view.html new file mode 100644 index 0000000..55065d1 --- /dev/null +++ b/app/templates/goals/view.html @@ -0,0 +1,188 @@ +{% extends "base.html" %} + +{% block title %}{{ goal.title }} - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+
+

{{ goal.title }}

+
+ 创建日期: {{ goal.created_at.strftime('%Y-%m-%d') if goal.created_at else '-' }} + {% if goal.end_date %} + 截止日期: {{ goal.end_date.strftime('%Y-%m-%d') }} + {% endif %} + 状态: + {% if goal.status == 'active' %} + 进行中 + {% elif goal.status == 'completed' %} + 已完成 + {% elif goal.status == 'paused' %} + 已暂停 + {% else %} + {{ goal.status }} + {% endif %} + +
+
+ +
+ + {% if goal.description %} +
+ 目标描述:
+ {{ goal.description }} +
+ {% endif %} +
+ +
+
+

{{ stats.total }}

+

总任务数

+
+
+

{{ stats.completed }}

+

已完成

+
+
+

{{ stats.in_progress }}

+

进行中

+
+
+

{{ stats.completion_rate }}%

+

完成率

+
+
+ +
+

进度概览

+
+
+
+

+ {% if stats.completion_rate == 100 %} + 恭喜!所有任务已完成! + {% elif stats.completion_rate >= 80 %} + 进度良好,继续保持! + {% elif stats.completion_rate >= 50 %} + 已过半,继续加油! + {% else %} + 还需要更多努力! + {% endif %} +

+
+ +
+
+

任务列表

+ 添加任务 +
+ + {% if tasks %} + {% for task in tasks %} +
+
+
+

+ + {% if task.status == 'completed' %}{% endif %} + {{ task.title }} + {% if task.status == 'completed' %}{% endif %} + +

+ {% if task.description %} +

{{ task.description[:100] }}{% if task.description|length > 100 %}...{% endif %}

+ {% endif %} +
+ 状态: + {% if task.status == 'pending' %} + 待开始 + {% elif task.status == 'in_progress' %} + 进行中 + {% elif task.status == 'completed' %} + 已完成 + {% else %} + {{ task.status }} + {% endif %} + + 优先级: + {% if task.priority == 'high' %} + + {% elif task.priority == 'medium' %} + + {% else %} + + {% endif %} + + 进度: {{ task.progress }}% + {% if task.deadline %} + 截止: {{ task.deadline.strftime('%Y-%m-%d') }} + {% endif %} + {% if task.subtasks|length > 0 %} + 子任务: {{ task.subtasks|length }} 个 + {% endif %} +
+
+
+
+
+
+
+
+ {% if task.status != 'completed' %} + + {% endif %} + 详情 + 添加子任务 +
+
+ + {% if task.subtasks %} +
+ 子任务: + {% for subtask in task.subtasks %} + + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + {% else %} +
+

还没有任何任务

+ 添加第一个任务 +
+ {% endif %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..4d160df --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}首页 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+

{{ today_hours }}

+

今日学习时长(小时)

+
+
+

{{ active_goals_count }}

+

进行中的学习目标

+
+
+

{{ pending_tasks|length }}

+

待完成的任务

+
+
+

{{ in_progress_tasks|length }}

+

进行中的任务

+
+
+ +
+
+

快速操作

+
+ +
+ +{% if in_progress_tasks %} +
+

进行中的任务

+ {% for task in in_progress_tasks %} +
+
+
+

{{ task.title }}

+
+ 所属目标: {{ task.goal.title }} + 优先级: + {% if task.priority == 'high' %} + + {% elif task.priority == 'medium' %} + + {% else %} + + {% endif %} + + {% if task.deadline %} + 截止日期: {{ task.deadline.strftime('%Y-%m-%d') }} + {% endif %} +
+
+
+
+
+ 进度: {{ task.progress }}% +
+
+
+ + 详情 +
+
+
+ {% endfor %} +
+{% endif %} + +{% if pending_tasks %} +
+

待开始的任务

+ {% for task in pending_tasks %} +
+
+
+

{{ task.title }}

+
+ 所属目标: {{ task.goal.title }} + 优先级: + {% if task.priority == 'high' %} + + {% elif task.priority == 'medium' %} + + {% else %} + + {% endif %} + + {% if task.deadline %} + 截止日期: {{ task.deadline.strftime('%Y-%m-%d') }} + {% endif %} +
+
+
+ + 详情 +
+
+
+ {% endfor %} +
+{% endif %} + +{% if not in_progress_tasks and not pending_tasks %} +
+
+

还没有任何学习任务

+ 创建第一个学习目标 +
+
+{% endif %} + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..2fc8c72 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}登录 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

用户登录

+
+
+ + +
+
+ + +
+
+ +
+ +
+

+ 还没有账号? 立即注册 +

+
+{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..2992a3b --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block title %}注册 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

用户注册

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ 已有账号? 立即登录 +

+
+{% endblock %} diff --git a/app/templates/reports/generate.html b/app/templates/reports/generate.html new file mode 100644 index 0000000..00088e3 --- /dev/null +++ b/app/templates/reports/generate.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block title %}生成学习报告 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

生成学习报告

+
+
+ + +
+ + + +
+ + 取消 +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/reports/history.html b/app/templates/reports/history.html new file mode 100644 index 0000000..f90f81f --- /dev/null +++ b/app/templates/reports/history.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}历史学习记录 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

总体统计

+
+
+

{{ stats.total_goals }}

+

总目标数

+
+
+

{{ stats.completed_goals }}

+

已完成目标

+
+
+

{{ stats.completion_rate }}%

+

目标完成率

+
+
+

{{ stats.total_study_hours }}

+

总学习时长(小时)

+
+
+
+ +
+

历史学习目标

+ + {% if goals.items %} +
+ + + + + + + + + + + + + {% for goal in goals.items %} + + + + + + + + + {% endfor %} + +
目标名称创建日期截止日期状态任务数操作
+ + {{ goal.title }} + + {% if goal.description %} +
+ {{ goal.description[:50] }}{% if goal.description|length > 50 %}...{% endif %} + {% endif %} +
{{ goal.created_at.strftime('%Y-%m-%d') if goal.created_at else '-' }}{{ goal.end_date.strftime('%Y-%m-%d') if goal.end_date else '-' }} + {% if goal.status == 'active' %} + 进行中 + {% elif goal.status == 'completed' %} + 已完成 + {% elif goal.status == 'paused' %} + 已暂停 + {% else %} + {{ goal.status }} + {% endif %} + {{ goal.tasks.count() }} + 查看详情 +
+
+ + {% if goals.pages > 1 %} + + {% endif %} + + {% else %} +
+

还没有任何学习目标

+ 创建第一个学习目标 +
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/reports/list.html b/app/templates/reports/list.html new file mode 100644 index 0000000..ff035bc --- /dev/null +++ b/app/templates/reports/list.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block title %}学习报告列表 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+

学习报告列表

+ 生成新报告 +
+ + {% if reports %} + + + + + + + + + + + + + {% for report in reports %} + + + + + + + + + {% endfor %} + +
报告类型时间范围学习时长完成任务生成时间操作
+ {% if report.report_type == 'weekly' %} + 周报 + {% elif report.report_type == 'monthly' %} + 月报 + {% else %} + 自定义 + {% endif %} + + {{ report.start_date.strftime('%Y-%m-%d') }} 至 {{ report.end_date.strftime('%Y-%m-%d') }} + {{ report.total_study_hours }} 小时{{ report.completed_tasks }} / {{ report.total_tasks }}{{ report.created_at.strftime('%Y-%m-%d %H:%M') if report.created_at else '-' }} + 查看 +
+ {% else %} +
+

还没有任何学习报告

+ 生成第一份报告 +
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/reports/view.html b/app/templates/reports/view.html new file mode 100644 index 0000000..a5a14a3 --- /dev/null +++ b/app/templates/reports/view.html @@ -0,0 +1,92 @@ +{% extends "base.html" %} + +{% block title %}学习报告 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+

学习报告详情

+
+ {% if report.report_type == 'weekly' %} + 周报 + {% elif report.report_type == 'monthly' %} + 月报 + {% else %} + 自定义报告 + {% endif %} +
+
+ +
+ 时间范围: {{ report.start_date.strftime('%Y-%m-%d') }} 至 {{ report.end_date.strftime('%Y-%m-%d') }} +
+ 生成时间: {{ report.created_at.strftime('%Y-%m-%d %H:%M') if report.created_at else '-' }} +
+
+ +
+
+

{{ report.total_study_hours }}

+

总学习时长(小时)

+
+
+

{{ report.completed_tasks }}

+

已完成任务数

+
+
+

{{ report.total_tasks }}

+

总任务数

+
+
+

+ {% if report.total_tasks > 0 %} + {{ ((report.completed_tasks / report.total_tasks) * 100)|round(1) }} + {% else %} + 0 + {% endif %}% +

+

任务完成率

+
+
+ +{% if report.goals_progress and report.goals_progress != '[]' %} +
+

各目标进度

+
+ {% set goals = report.goals_progress|replace("[", "")|replace("]", "")|replace("{", "")|replace("}", "")|replace("'", "")|split(", ") %} + {% for goal_str in goals %} + {% if goal_str %} + {% set parts = goal_str|split(": ") %} + {% if parts|length >= 2 %} + {% set key = parts[0] %} + {% set value = parts[1:]|join(": ") %} +
+ {{ key }}: {{ value }} +
+ {% endif %} + {% endif %} + {% endfor %} +
+
+{% endif %} + +{% if report.recommendations %} +
+

学习建议

+
+ {% for line in report.recommendations.split('\n\n') %} +

+ 💡 {{ line }} +

+ {% endfor %} +
+
+{% endif %} + + +{% endblock %} diff --git a/app/templates/tasks/create.html b/app/templates/tasks/create.html new file mode 100644 index 0000000..cf15fd0 --- /dev/null +++ b/app/templates/tasks/create.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block title %}创建学习任务 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

创建学习任务

+
+ 所属目标: {{ goal.title }} + {% if parent_task %} +
父任务: {{ parent_task.title }} + {% endif %} +
+
+ {% if parent_task %} + + {% endif %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + 取消 +
+
+
+{% endblock %} diff --git a/app/templates/tasks/edit.html b/app/templates/tasks/edit.html new file mode 100644 index 0000000..aa1caec --- /dev/null +++ b/app/templates/tasks/edit.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block title %}编辑学习任务 - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+

编辑学习任务

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + 取消 +
+
+
+{% endblock %} diff --git a/app/templates/tasks/view.html b/app/templates/tasks/view.html new file mode 100644 index 0000000..7a45b30 --- /dev/null +++ b/app/templates/tasks/view.html @@ -0,0 +1,361 @@ +{% extends "base.html" %} + +{% block title %}{{ task.title }} - 个人学习计划管理系统{% endblock %} + +{% block content %} +
+
+
+

{% if task.status == 'completed' %}{% endif %}{{ task.title }}{% if task.status == 'completed' %}{% endif %}

+
+ 所属目标: {{ task.goal.title }} + 创建日期: {{ task.created_at.strftime('%Y-%m-%d') if task.created_at else '-' }} + 状态: + {% if task.status == 'pending' %} + 待开始 + {% elif task.status == 'in_progress' %} + 进行中 + {% elif task.status == 'completed' %} + 已完成 + {% else %} + {{ task.status }} + {% endif %} + + 优先级: + {% if task.priority == 'high' %} + + {% elif task.priority == 'medium' %} + + {% else %} + + {% endif %} + +
+
+ +
+ + {% if task.description %} +
+ 任务描述:
+ {{ task.description }} +
+ {% endif %} +
+ +
+
+

{{ task.progress }}%

+

完成进度

+
+
+

{{ task.estimated_hours }}

+

预计时长(小时)

+
+
+

{{ total_hours }}

+

实际学习(小时)

+
+
+

{{ subtasks|length }}

+

子任务数

+
+
+ +
+

学习计时

+
+
00:00:00
+

点击开始学习按钮开始计时

+
+
+ + +
+
+ +
+

更新进度

+
+
+
+
+ + {{ task.progress }}% + +
+
+ + + + + +
+
+ +{% if subtasks %} +
+
+

子任务

+ 添加子任务 +
+ {% for subtask in subtasks %} + + {% endfor %} +
+{% endif %} + +{% if study_records %} +
+

学习记录

+ + + + + + + + + + + {% for record in study_records %} + + + + + + + {% endfor %} + +
开始时间结束时间时长备注
{{ record.start_time.strftime('%Y-%m-%d %H:%M') if record.start_time else '-' }}{{ record.end_time.strftime('%Y-%m-%d %H:%M') if record.end_time else '进行中' }} + {% if record.duration_minutes %} + {% set hours = record.duration_minutes // 60 %} + {% set minutes = record.duration_minutes % 60 %} + {% if hours > 0 %}{{ hours }}小时{% endif %}{{ minutes }}分钟 + {% else %} + - + {% endif %} + {{ record.notes or '-' }}
+
+{% endif %} + +
+

任务提醒

+ {% if reminders %} + + + + + + + + + + + {% for reminder in reminders %} + + + + + + + {% endfor %} + +
提醒时间消息状态操作
{{ reminder.reminder_time.strftime('%Y-%m-%d %H:%M') if reminder.reminder_time else '-' }}{{ reminder.message or '-' }} + {% if reminder.is_sent %} + 已发送 + {% else %} + 待发送 + {% endif %} + +
+ +
+
+ {% else %} +

还没有设置任何提醒

+ {% endif %} + +

添加新提醒

+
+
+ + +
+
+ + +
+ +
+
+ +
+

危险操作

+

删除此任务将同时删除所有关联的子任务、学习记录和提醒,此操作不可撤销。

+
+ +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..3eb1f41 --- /dev/null +++ b/config.py @@ -0,0 +1,9 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'study-plan-secret-key-2024' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'study_plan.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..069b4cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask>=3.0.0 +Flask-SQLAlchemy>=3.1.1 +Flask-Login>=0.6.3 +Flask-WTF>=1.2.1 +Werkzeug>=3.0.0 +SQLAlchemy>=2.0.30 +python-dateutil>=2.8.2 diff --git a/run.py b/run.py new file mode 100644 index 0000000..ecad898 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5001) diff --git a/study_plan.db b/study_plan.db new file mode 100644 index 0000000000000000000000000000000000000000..67d6c1bc69fac8dcc4d626c64bd801590a12a652 GIT binary patch literal 36864 zcmeI&O>f&a7zc2vP2!iK?JkC|yacg;uE^4t+pxJQ(;9Q-MRnTrBxn|w2(u)Eq_GA& zwF%nWfF1Uo_96CR#z&4EF^%1q0R?9K4LG()iWYy*^GG0&XZ!s`3-(Jc(@3+s&ex9X zIzKSxI8NQX8|GcU=yFrRMa`=YOP=gwb$-!Q%P+O~7+_RW88p5FNGt3Mlm)DKMq zUJ!r)1Rwx`|B1kx&3di9wdI~QboBFB=t%tAksItF<>b-7~LnQhFMn7^qP$!4=2 zSzXKE5pZQ>oZqf6y%=5RLTIT+*f1SVZwS$u7!bNw7wFIx&^|>A8Py zadqxf=am&>D_wnZrEpHY^`dazwyeULUJBnuNJzp;kl9)9H50cA=NVPjC*5Jv)LchV z&;Pk@&%vyqQCoSF+g+PI47gpcbaC^Vq=R)UT2|MRaOqDk*JTmDB6?IM?*(mf0e|K* z;{%E>u5YxzX}Q1CGxSyRDlCT6pDaoReXgDB9r(Td18(jfkN6?GJdh21MuS2m60y0R$ib0SG_< z0uX=z1Rwwb2tZ)<1@Qm>s~=;m5CRZ@00bZa0SG_<0uX=z1R#L@KXL#95P$##AOHaf zKmY;|fB*y_u=)bn|F3?Gu|fzy00Izz00bZa0SG_<0uX=z_W#HM2tWV=5P$##AOHaf mKmY;|fWYbtVE@1RF~$lZ009U<00Izz00bZa0SG_<0{;SekFRt9 literal 0 HcmV?d00001