from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException, Depends from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, RedirectResponse from sqlmodel import Session from pathlib import Path import uuid, os, calendar from datetime import datetime from database import engine, init_db, get_session from models import Subject, ErrorImage, Evaluation import crud app = FastAPI(title="满意在线错题本") app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") init_db() UPLOAD_DIR = Path("static/uploads") UPLOAD_DIR.mkdir(parents=True, exist_ok=True) COLORS = ["#6366f1","#8b5cf6","#ec4899","#f43f5e","#f97316","#eab308","#22c55e","#14b8a6","#06b6d4","#3b82f6"] # ── 首页 ───────────────────────────────────────────── @app.get("/", response_class=HTMLResponse) async def index(request: Request, session: Session = Depends(get_session)): subjects = crud.get_all_subjects(session) today = datetime.now().strftime("%Y-%m-%d") today_images = {} for s in subjects: imgs = crud.get_subject_images(session, s.id, date=today) if imgs: today_images[s.id] = imgs evs = crud.get_evaluations(session) return templates.TemplateResponse("index.html", { "request": request, "subjects": subjects, "today": today, "today_images": today_images, "colors": COLORS, "evaluations": evs }) # ── 科目管理 ──────────────────────────────────────────── @app.get("/subjects", response_class=HTMLResponse) async def subjects_page(request: Request, session: Session = Depends(get_session)): subjects = crud.get_all_subjects(session) return templates.TemplateResponse("subjects.html", {"request": request, "subjects": subjects, "colors": COLORS}) @app.post("/subjects/create") async def create_subject( request: Request, name: str = Form(...), color: str = Form(default="#6366f1"), session: Session = Depends(get_session) ): subject = crud.create_subject(session, name.strip(), color) return RedirectResponse(url=f"/subject/{subject.id}", status_code=303) @app.post("/subjects/{subject_id}/delete") async def delete_subject(subject_id: int, session: Session = Depends(get_session)): if crud.delete_subject(session, subject_id): return RedirectResponse(url="/subjects", status_code=303) raise HTTPException(404, "科目不存在") # ── 科目详情页 ───────────────────────────────────────── @app.get("/subject/{subject_id}", response_class=HTMLResponse) async def subject_detail( request: Request, subject_id: int, date: str | None = None, session: Session = Depends(get_session) ): subject = session.get(Subject, subject_id) if not subject: raise HTTPException(404, "科目不存在") dates = crud.get_dates_for_subject(session, subject_id) today = datetime.now().strftime("%Y-%m-%d") if not date: date = today images = crud.get_subject_images(session, subject_id, date) prev_date = None next_date = None if dates and date in dates: idx = dates.index(date) if idx < len(dates) - 1: prev_date = dates[idx + 1] if idx > 0: next_date = dates[idx - 1] return templates.TemplateResponse("subject_detail.html", { "request": request, "subject": subject, "dates": dates, "current_date": date, "today": today, "images": images, "prev_date": prev_date, "next_date": next_date, "subjects": crud.get_all_subjects(session) }) # ── 上传错题图片 ──────────────────────────────────────── @app.post("/subject/{subject_id}/upload") async def upload_error_images( subject_id: int, date: str = Form(...), files: list[UploadFile] = File(...), session: Session = Depends(get_session) ): subject = session.get(Subject, subject_id) if not subject: raise HTTPException(404, "科目不存在") filenames, original_names = [], [] for f in files: content = await f.read() ext = Path(f.filename).suffix or ".jpg" unique_name = f"{uuid.uuid4().hex}{ext}" dest = UPLOAD_DIR / unique_name dest.write_bytes(content) filenames.append(unique_name) original_names.append(f.filename) crud.add_error_images(session, subject_id, date, filenames, original_names) return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303) # ── 录入文字错题 ──────────────────────────────────────── @app.post("/subject/{subject_id}/text") async def add_error_text( subject_id: int, date: str = Form(...), question: str = Form(...), answer: str = Form(default=""), session: Session = Depends(get_session) ): subject = session.get(Subject, subject_id) if not subject: raise HTTPException(404, "科目不存在") crud.add_error_text(session, subject_id, date, question, answer) return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303) # ── 删除单张图片 ──────────────────────────────────────── @app.post("/image/{image_id}/delete") async def delete_image(image_id: int, session: Session = Depends(get_session)): img = crud.delete_image(session, image_id) if not img: raise HTTPException(404, "图片不存在") return RedirectResponse(url=f"/subject/{img.subject_id}?date={img.date}", status_code=303) # ── 时间线 ─────────────────────────────────────────────── @app.get("/timeline", response_class=HTMLResponse) async def timeline(request: Request, date: str | None = None, session: Session = Depends(get_session)): subjects = crud.get_all_subjects(session) all_dates = crud.get_all_dates(session) if not date: date = all_dates[0] if all_dates else datetime.now().strftime("%Y-%m-%d") timeline_data = [] for s in subjects: imgs = crud.get_subject_images(session, s.id, date) if imgs: timeline_data.append({"subject": s, "images": imgs}) prev_date = None next_date = None if all_dates and date in all_dates: idx = all_dates.index(date) if idx < len(all_dates) - 1: prev_date = all_dates[idx + 1] if idx > 0: next_date = all_dates[idx - 1] return templates.TemplateResponse("timeline.html", { "request": request, "subjects": subjects, "all_dates": all_dates, "current_date": date, "timeline_data": timeline_data, "prev_date": prev_date, "next_date": next_date }) # ── 复习汇总 ──────────────────────────────────────────── # ── 时间线 New(对比样式) ─────────────────────────── @app.get("/timeline-new", response_class=HTMLResponse) async def timeline_new(request: Request, date: str | None = None, session: Session = Depends(get_session)): subjects = crud.get_all_subjects(session) all_dates = crud.get_all_dates(session) if not date: date = all_dates[0] if all_dates else datetime.now().strftime("%Y-%m-%d") nodes = [] for d in all_dates: items = [] for s in subjects: imgs = crud.get_subject_images(session, s.id, d) if imgs: image_items = [img for img in imgs if img.filename] text_items = [img for img in imgs if not img.filename] items.append({"subject": s, "images": image_items, "texts": text_items}) if not items: continue nodes.append({ "date": d, "active": d == date, "total": sum(len(it["images"]) + len(it["texts"]) for it in items), "image_count": sum(len(it["images"]) for it in items), "text_count": sum(len(it["texts"]) for it in items), "subject_count": len(items), "preview": next((img for it in items for img in it["images"]), None), "preview_text": next((txt for it in items for txt in it["texts"]), None), "items": items, "week": "周" + "一二三四五六日"[calendar.weekday(*[int(x) for x in d.split("-")])], }) active_node = next((n for n in nodes if n["active"]), None) total_records = sum(n["total"] for n in nodes) total_images = sum(n["image_count"] for n in nodes) total_texts = sum(n["text_count"] for n in nodes) prev_date = None next_date = None if all_dates and date in all_dates: idx = all_dates.index(date) if idx < len(all_dates) - 1: prev_date = all_dates[idx + 1] if idx > 0: next_date = all_dates[idx - 1] return templates.TemplateResponse("timeline_new.html", { "request": request, "subjects": subjects, "all_dates": all_dates, "current_date": date, "nodes": nodes, "active_node": active_node, "total_records": total_records, "total_images": total_images, "total_texts": total_texts, "prev_date": prev_date, "next_date": next_date }) @app.get("/review", response_class=HTMLResponse) async def review_page(request: Request, session: Session = Depends(get_session)): data = crud.get_review_data(session) subjects = crud.get_all_subjects(session) return templates.TemplateResponse("review.html", { "request": request, "review_data": data, "subjects": subjects }) # ── 知识评价 ──────────────────────────────────────────── @app.get("/evaluations", response_class=HTMLResponse) async def evaluations_page(request: Request, subject_id: int | None = None, session: Session = Depends(get_session)): subjects = crud.get_all_subjects(session) evs = crud.get_evaluations(session, subject_id) return templates.TemplateResponse("evaluations.html", { "request": request, "subjects": subjects, "evaluations": evs, "current_subject": subject_id, "today": datetime.now().strftime("%Y-%m-%d") }) @app.post("/evaluations/add") async def add_evaluation( subject_id: int = Form(...), date: str = Form(...), title: str = Form(...), content: str = Form(default=""), file: UploadFile | None = File(None), session: Session = Depends(get_session) ): filename = None if file and file.filename: ext = Path(file.filename).suffix or ".pdf" unique_name = f"eval_{uuid.uuid4().hex}{ext}" dest = UPLOAD_DIR / unique_name dest.write_bytes(await file.read()) filename = unique_name crud.add_evaluation(session, subject_id, date, title, content, filename) return RedirectResponse(url=f"/evaluations?subject_id={subject_id}", status_code=303) @app.post("/evaluation/{eval_id}/delete") async def delete_evaluation(eval_id: int, session: Session = Depends(get_session)): ev = session.get(Evaluation, eval_id) if not ev: raise HTTPException(404) if ev.filename: p = UPLOAD_DIR / ev.filename if p.exists(): p.unlink() session.delete(ev) session.commit() return RedirectResponse(url=f"/evaluations?subject_id={ev.subject_id}", status_code=303) if __name__ == "__main__": import uvicorn uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)