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 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) if not date: date = dates[0] if dates else datetime.now().strftime("%Y-%m-%d") images = crud.get_subject_images(session, subject_id, date) if not images and dates: date = dates[0] 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, "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("/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 }) # ── 复习汇总 ──────────────────────────────────────────── @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)