|
@@ -0,0 +1,204 @@
|
|
|
|
|
+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
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+# ── 上传错题图片 ────────────────────────────────────────
|
|
|
|
|
+@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)
|