app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException, Depends
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.templating import Jinja2Templates
  4. from fastapi.responses import HTMLResponse, RedirectResponse
  5. from sqlmodel import Session
  6. from pathlib import Path
  7. import uuid, os, calendar
  8. from datetime import datetime
  9. from database import engine, init_db, get_session
  10. from models import Subject, ErrorImage, Evaluation
  11. import crud
  12. app = FastAPI(title="满意在线错题本")
  13. app.mount("/static", StaticFiles(directory="static"), name="static")
  14. templates = Jinja2Templates(directory="templates")
  15. init_db()
  16. UPLOAD_DIR = Path("static/uploads")
  17. UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
  18. COLORS = ["#6366f1","#8b5cf6","#ec4899","#f43f5e","#f97316","#eab308","#22c55e","#14b8a6","#06b6d4","#3b82f6"]
  19. # ── 首页 ─────────────────────────────────────────────
  20. @app.get("/", response_class=HTMLResponse)
  21. async def index(request: Request, session: Session = Depends(get_session)):
  22. subjects = crud.get_all_subjects(session)
  23. today = datetime.now().strftime("%Y-%m-%d")
  24. today_images = {}
  25. for s in subjects:
  26. imgs = crud.get_subject_images(session, s.id, date=today)
  27. if imgs:
  28. today_images[s.id] = imgs
  29. evs = crud.get_evaluations(session)
  30. return templates.TemplateResponse("index.html", {
  31. "request": request, "subjects": subjects,
  32. "today": today, "today_images": today_images, "colors": COLORS,
  33. "evaluations": evs
  34. })
  35. # ── 科目管理 ────────────────────────────────────────────
  36. @app.get("/subjects", response_class=HTMLResponse)
  37. async def subjects_page(request: Request, session: Session = Depends(get_session)):
  38. subjects = crud.get_all_subjects(session)
  39. return templates.TemplateResponse("subjects.html", {"request": request, "subjects": subjects, "colors": COLORS})
  40. @app.post("/subjects/create")
  41. async def create_subject(
  42. request: Request,
  43. name: str = Form(...),
  44. color: str = Form(default="#6366f1"),
  45. session: Session = Depends(get_session)
  46. ):
  47. subject = crud.create_subject(session, name.strip(), color)
  48. return RedirectResponse(url=f"/subject/{subject.id}", status_code=303)
  49. @app.post("/subjects/{subject_id}/delete")
  50. async def delete_subject(subject_id: int, session: Session = Depends(get_session)):
  51. if crud.delete_subject(session, subject_id):
  52. return RedirectResponse(url="/subjects", status_code=303)
  53. raise HTTPException(404, "科目不存在")
  54. # ── 科目详情页 ─────────────────────────────────────────
  55. @app.get("/subject/{subject_id}", response_class=HTMLResponse)
  56. async def subject_detail(
  57. request: Request, subject_id: int, date: str | None = None,
  58. session: Session = Depends(get_session)
  59. ):
  60. subject = session.get(Subject, subject_id)
  61. if not subject:
  62. raise HTTPException(404, "科目不存在")
  63. dates = crud.get_dates_for_subject(session, subject_id)
  64. today = datetime.now().strftime("%Y-%m-%d")
  65. if not date:
  66. date = today
  67. images = crud.get_subject_images(session, subject_id, date)
  68. prev_date = None
  69. next_date = None
  70. if dates and date in dates:
  71. idx = dates.index(date)
  72. if idx < len(dates) - 1:
  73. prev_date = dates[idx + 1]
  74. if idx > 0:
  75. next_date = dates[idx - 1]
  76. return templates.TemplateResponse("subject_detail.html", {
  77. "request": request, "subject": subject, "dates": dates,
  78. "current_date": date, "today": today, "images": images,
  79. "prev_date": prev_date, "next_date": next_date,
  80. "subjects": crud.get_all_subjects(session)
  81. })
  82. # ── 上传错题图片 ────────────────────────────────────────
  83. @app.post("/subject/{subject_id}/upload")
  84. async def upload_error_images(
  85. subject_id: int,
  86. date: str = Form(...),
  87. files: list[UploadFile] = File(...),
  88. session: Session = Depends(get_session)
  89. ):
  90. subject = session.get(Subject, subject_id)
  91. if not subject:
  92. raise HTTPException(404, "科目不存在")
  93. filenames, original_names = [], []
  94. for f in files:
  95. content = await f.read()
  96. ext = Path(f.filename).suffix or ".jpg"
  97. unique_name = f"{uuid.uuid4().hex}{ext}"
  98. dest = UPLOAD_DIR / unique_name
  99. dest.write_bytes(content)
  100. filenames.append(unique_name)
  101. original_names.append(f.filename)
  102. crud.add_error_images(session, subject_id, date, filenames, original_names)
  103. return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303)
  104. # ── 录入文字错题 ────────────────────────────────────────
  105. @app.post("/subject/{subject_id}/text")
  106. async def add_error_text(
  107. subject_id: int,
  108. date: str = Form(...),
  109. question: str = Form(...),
  110. answer: str = Form(default=""),
  111. session: Session = Depends(get_session)
  112. ):
  113. subject = session.get(Subject, subject_id)
  114. if not subject:
  115. raise HTTPException(404, "科目不存在")
  116. crud.add_error_text(session, subject_id, date, question, answer)
  117. return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303)
  118. # ── 删除单张图片 ────────────────────────────────────────
  119. @app.post("/image/{image_id}/delete")
  120. async def delete_image(image_id: int, session: Session = Depends(get_session)):
  121. img = crud.delete_image(session, image_id)
  122. if not img:
  123. raise HTTPException(404, "图片不存在")
  124. return RedirectResponse(url=f"/subject/{img.subject_id}?date={img.date}", status_code=303)
  125. # ── 时间线 ───────────────────────────────────────────────
  126. @app.get("/timeline", response_class=HTMLResponse)
  127. async def timeline(request: Request, date: str | None = None, session: Session = Depends(get_session)):
  128. subjects = crud.get_all_subjects(session)
  129. all_dates = crud.get_all_dates(session)
  130. if not date:
  131. date = all_dates[0] if all_dates else datetime.now().strftime("%Y-%m-%d")
  132. timeline_data = []
  133. for s in subjects:
  134. imgs = crud.get_subject_images(session, s.id, date)
  135. if imgs:
  136. timeline_data.append({"subject": s, "images": imgs})
  137. prev_date = None
  138. next_date = None
  139. if all_dates and date in all_dates:
  140. idx = all_dates.index(date)
  141. if idx < len(all_dates) - 1:
  142. prev_date = all_dates[idx + 1]
  143. if idx > 0:
  144. next_date = all_dates[idx - 1]
  145. return templates.TemplateResponse("timeline.html", {
  146. "request": request, "subjects": subjects, "all_dates": all_dates,
  147. "current_date": date, "timeline_data": timeline_data,
  148. "prev_date": prev_date, "next_date": next_date
  149. })
  150. # ── 复习汇总 ────────────────────────────────────────────
  151. # ── 时间线 New(对比样式) ───────────────────────────
  152. @app.get("/timeline-new", response_class=HTMLResponse)
  153. async def timeline_new(request: Request, date: str | None = None, session: Session = Depends(get_session)):
  154. subjects = crud.get_all_subjects(session)
  155. all_dates = crud.get_all_dates(session)
  156. if not date:
  157. date = all_dates[0] if all_dates else datetime.now().strftime("%Y-%m-%d")
  158. nodes = []
  159. for d in all_dates:
  160. items = []
  161. for s in subjects:
  162. imgs = crud.get_subject_images(session, s.id, d)
  163. if imgs:
  164. image_items = [img for img in imgs if img.filename]
  165. text_items = [img for img in imgs if not img.filename]
  166. items.append({"subject": s, "images": image_items, "texts": text_items})
  167. if not items:
  168. continue
  169. nodes.append({
  170. "date": d,
  171. "active": d == date,
  172. "total": sum(len(it["images"]) + len(it["texts"]) for it in items),
  173. "image_count": sum(len(it["images"]) for it in items),
  174. "text_count": sum(len(it["texts"]) for it in items),
  175. "subject_count": len(items),
  176. "preview": next((img for it in items for img in it["images"]), None),
  177. "preview_text": next((txt for it in items for txt in it["texts"]), None),
  178. "items": items,
  179. "week": "周" + "一二三四五六日"[calendar.weekday(*[int(x) for x in d.split("-")])],
  180. })
  181. active_node = next((n for n in nodes if n["active"]), None)
  182. total_records = sum(n["total"] for n in nodes)
  183. total_images = sum(n["image_count"] for n in nodes)
  184. total_texts = sum(n["text_count"] for n in nodes)
  185. prev_date = None
  186. next_date = None
  187. if all_dates and date in all_dates:
  188. idx = all_dates.index(date)
  189. if idx < len(all_dates) - 1:
  190. prev_date = all_dates[idx + 1]
  191. if idx > 0:
  192. next_date = all_dates[idx - 1]
  193. return templates.TemplateResponse("timeline_new.html", {
  194. "request": request, "subjects": subjects, "all_dates": all_dates,
  195. "current_date": date, "nodes": nodes,
  196. "active_node": active_node, "total_records": total_records,
  197. "total_images": total_images, "total_texts": total_texts,
  198. "prev_date": prev_date, "next_date": next_date
  199. })
  200. @app.get("/review", response_class=HTMLResponse)
  201. async def review_page(request: Request, session: Session = Depends(get_session)):
  202. data = crud.get_review_data(session)
  203. subjects = crud.get_all_subjects(session)
  204. return templates.TemplateResponse("review.html", {
  205. "request": request, "review_data": data, "subjects": subjects
  206. })
  207. # ── 知识评价 ────────────────────────────────────────────
  208. @app.get("/evaluations", response_class=HTMLResponse)
  209. async def evaluations_page(request: Request, subject_id: int | None = None, session: Session = Depends(get_session)):
  210. subjects = crud.get_all_subjects(session)
  211. evs = crud.get_evaluations(session, subject_id)
  212. return templates.TemplateResponse("evaluations.html", {
  213. "request": request, "subjects": subjects, "evaluations": evs,
  214. "current_subject": subject_id, "today": datetime.now().strftime("%Y-%m-%d")
  215. })
  216. @app.post("/evaluations/add")
  217. async def add_evaluation(
  218. subject_id: int = Form(...),
  219. date: str = Form(...),
  220. title: str = Form(...),
  221. content: str = Form(default=""),
  222. file: UploadFile | None = File(None),
  223. session: Session = Depends(get_session)
  224. ):
  225. filename = None
  226. if file and file.filename:
  227. ext = Path(file.filename).suffix or ".pdf"
  228. unique_name = f"eval_{uuid.uuid4().hex}{ext}"
  229. dest = UPLOAD_DIR / unique_name
  230. dest.write_bytes(await file.read())
  231. filename = unique_name
  232. crud.add_evaluation(session, subject_id, date, title, content, filename)
  233. return RedirectResponse(url=f"/evaluations?subject_id={subject_id}", status_code=303)
  234. @app.post("/evaluation/{eval_id}/delete")
  235. async def delete_evaluation(eval_id: int, session: Session = Depends(get_session)):
  236. ev = session.get(Evaluation, eval_id)
  237. if not ev:
  238. raise HTTPException(404)
  239. if ev.filename:
  240. p = UPLOAD_DIR / ev.filename
  241. if p.exists():
  242. p.unlink()
  243. session.delete(ev)
  244. session.commit()
  245. return RedirectResponse(url=f"/evaluations?subject_id={ev.subject_id}", status_code=303)
  246. if __name__ == "__main__":
  247. import uvicorn
  248. uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)