2026-04-19 12:13:07 +02:00
|
|
|
from contextlib import asynccontextmanager
|
2026-04-23 15:23:20 +02:00
|
|
|
from pathlib import Path
|
2026-04-19 12:13:07 +02:00
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
|
2026-04-23 15:23:20 +02:00
|
|
|
from fastapi.responses import FileResponse, RedirectResponse, Response
|
2026-04-19 12:13:07 +02:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
from fastapi.templating import Jinja2Templates
|
2026-04-19 13:00:11 +02:00
|
|
|
from sqlalchemy import func, or_
|
2026-04-19 12:36:55 +02:00
|
|
|
from sqlalchemy.orm import Session
|
2026-04-19 12:13:07 +02:00
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
from app.db import get_db, init_db
|
2026-04-19 12:54:25 +02:00
|
|
|
from app.images import process_upload
|
2026-04-19 12:36:55 +02:00
|
|
|
from app.models import Box, Item, SubItem
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
templates = Jinja2Templates(directory="app/templates")
|
2026-04-23 15:23:20 +02:00
|
|
|
STATIC_DIR = Path("app/static")
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
def _clean_text(value: str | None) -> str | None:
|
|
|
|
|
if value is None:
|
|
|
|
|
return None
|
|
|
|
|
cleaned = value.strip()
|
|
|
|
|
return cleaned or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_quantity(value: str | None) -> int | None:
|
|
|
|
|
cleaned = _clean_text(value)
|
|
|
|
|
if cleaned is None:
|
|
|
|
|
return None
|
|
|
|
|
return int(cleaned)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_checked(value: str | None) -> bool:
|
|
|
|
|
return value == "on"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_box_or_404(db: Session, box_id: int) -> Box:
|
|
|
|
|
box = db.get(Box, box_id)
|
|
|
|
|
if box is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Box not found")
|
|
|
|
|
return box
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_item_or_404(db: Session, item_id: int) -> Item:
|
|
|
|
|
item = db.get(Item, item_id)
|
|
|
|
|
if item is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_subitem_or_404(db: Session, subitem_id: int) -> SubItem:
|
|
|
|
|
subitem = db.get(SubItem, subitem_id)
|
|
|
|
|
if subitem is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Sub-item not found")
|
|
|
|
|
return subitem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _require_container_item(item: Item) -> None:
|
|
|
|
|
if not item.is_container:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Only container items can have sub-items")
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
def _set_image_fields(target, processed_image) -> None:
|
|
|
|
|
if processed_image is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
target.image_blob = processed_image.blob
|
|
|
|
|
target.image_mime_type = processed_image.mime_type
|
|
|
|
|
target.image_width = processed_image.width
|
|
|
|
|
target.image_height = processed_image.height
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _clear_image_fields(target) -> None:
|
|
|
|
|
target.image_blob = None
|
|
|
|
|
target.image_mime_type = None
|
|
|
|
|
target.image_width = None
|
|
|
|
|
target.image_height = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _image_response_or_404(target) -> Response:
|
|
|
|
|
if target.image_blob is None or target.image_mime_type is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Image not found")
|
|
|
|
|
|
|
|
|
|
return Response(content=target.image_blob, media_type=target.image_mime_type)
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 13:31:17 +02:00
|
|
|
def _wants_add_next(submit_action: str | None) -> bool:
|
|
|
|
|
return submit_action == "save_and_add_next"
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 13:00:11 +02:00
|
|
|
def _build_search_results(db: Session, query: str) -> list[dict]:
|
|
|
|
|
keyword = f"%{query.lower()}%"
|
|
|
|
|
results: list[dict] = []
|
|
|
|
|
|
|
|
|
|
box_matches = (
|
|
|
|
|
db.query(Box)
|
|
|
|
|
.filter(
|
|
|
|
|
or_(
|
|
|
|
|
func.lower(Box.name).like(keyword),
|
|
|
|
|
func.lower(func.coalesce(Box.note, "")).like(keyword),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.order_by(Box.id.desc())
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
for box in box_matches:
|
|
|
|
|
results.append(
|
|
|
|
|
{
|
|
|
|
|
"type": "Box",
|
|
|
|
|
"name": box.name,
|
|
|
|
|
"note": box.note,
|
|
|
|
|
"detail_url": f"/boxes/{box.id}",
|
|
|
|
|
"detail_label": "查看箱子",
|
|
|
|
|
"secondary_url": None,
|
|
|
|
|
"secondary_label": None,
|
|
|
|
|
"path": "顶层箱子",
|
|
|
|
|
"is_container": None,
|
|
|
|
|
"image_url": f"/boxes/{box.id}/image" if box.image_blob else None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
item_matches = (
|
|
|
|
|
db.query(Item)
|
|
|
|
|
.join(Item.box)
|
|
|
|
|
.filter(
|
|
|
|
|
or_(
|
|
|
|
|
func.lower(Item.name).like(keyword),
|
|
|
|
|
func.lower(func.coalesce(Item.note, "")).like(keyword),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.order_by(Item.id.desc())
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
for item in item_matches:
|
|
|
|
|
results.append(
|
|
|
|
|
{
|
|
|
|
|
"type": "Item",
|
|
|
|
|
"name": item.name,
|
|
|
|
|
"note": item.note,
|
|
|
|
|
"detail_url": f"/items/{item.id}",
|
|
|
|
|
"detail_label": "查看物品",
|
|
|
|
|
"secondary_url": f"/boxes/{item.box.id}",
|
|
|
|
|
"secondary_label": "查看所属箱子",
|
|
|
|
|
"path": f"位于箱子:{item.box.name}",
|
|
|
|
|
"is_container": item.is_container,
|
|
|
|
|
"image_url": f"/items/{item.id}/image" if item.image_blob else None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
subitem_matches = (
|
|
|
|
|
db.query(SubItem)
|
|
|
|
|
.join(SubItem.parent_item)
|
|
|
|
|
.join(Item.box)
|
|
|
|
|
.filter(
|
|
|
|
|
or_(
|
|
|
|
|
func.lower(SubItem.name).like(keyword),
|
|
|
|
|
func.lower(func.coalesce(SubItem.note, "")).like(keyword),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.order_by(SubItem.id.desc())
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
for subitem in subitem_matches:
|
|
|
|
|
results.append(
|
|
|
|
|
{
|
|
|
|
|
"type": "SubItem",
|
|
|
|
|
"name": subitem.name,
|
|
|
|
|
"note": subitem.note,
|
|
|
|
|
"detail_url": f"/items/{subitem.parent_item.id}",
|
|
|
|
|
"detail_label": "查看所属物品",
|
|
|
|
|
"secondary_url": f"/boxes/{subitem.parent_item.box.id}",
|
|
|
|
|
"secondary_label": "查看所属箱子",
|
|
|
|
|
"path": (
|
|
|
|
|
f"位于物品:{subitem.parent_item.name} / "
|
|
|
|
|
f"箱子:{subitem.parent_item.box.name}"
|
|
|
|
|
),
|
|
|
|
|
"is_container": None,
|
|
|
|
|
"image_url": f"/subitems/{subitem.id}/image" if subitem.image_blob else None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
2026-04-19 12:13:07 +02:00
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
init_db()
|
|
|
|
|
yield
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
app = FastAPI(title="搬家助手", lifespan=lifespan)
|
2026-04-19 12:13:07 +02:00
|
|
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
|
|
|
|
|
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
|
|
|
def root() -> RedirectResponse:
|
2026-04-19 12:36:55 +02:00
|
|
|
return RedirectResponse(url="/boxes", status_code=status.HTTP_302_FOUND)
|
2026-04-19 12:13:07 +02:00
|
|
|
|
2026-04-23 15:23:20 +02:00
|
|
|
@app.get("/manifest.webmanifest", include_in_schema=False)
|
|
|
|
|
def manifest() -> FileResponse:
|
|
|
|
|
return FileResponse(
|
|
|
|
|
path=STATIC_DIR / "manifest.webmanifest",
|
|
|
|
|
media_type="application/manifest+json",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.get("/service-worker.js", include_in_schema=False)
|
|
|
|
|
def service_worker() -> FileResponse:
|
|
|
|
|
return FileResponse(
|
|
|
|
|
path=STATIC_DIR / "service-worker.js",
|
|
|
|
|
media_type="application/javascript",
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 13:00:11 +02:00
|
|
|
@app.get("/search")
|
|
|
|
|
def search_page(
|
|
|
|
|
request: Request,
|
|
|
|
|
q: str | None = None,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
query = (q or "").strip()
|
|
|
|
|
results = _build_search_results(db, query) if query else []
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="search/index.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": "搜索",
|
|
|
|
|
"query": query,
|
|
|
|
|
"results": results,
|
|
|
|
|
"searched": bool(query),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 12:13:07 +02:00
|
|
|
@app.get("/boxes")
|
2026-04-19 12:36:55 +02:00
|
|
|
def list_boxes(request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
boxes = db.query(Box).order_by(Box.id.desc()).all()
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="boxes/index.html",
|
|
|
|
|
context={"page_title": "箱子", "boxes": boxes},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.get("/boxes/new")
|
|
|
|
|
def new_box_page(request: Request):
|
2026-04-19 12:13:07 +02:00
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
2026-04-19 12:36:55 +02:00
|
|
|
name="boxes/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": "新建箱子",
|
|
|
|
|
"box": None,
|
|
|
|
|
"form_action": "/boxes",
|
|
|
|
|
"submit_label": "创建箱子",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/boxes")
|
|
|
|
|
def create_box(
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
room: str | None = Form(default=None),
|
|
|
|
|
status_text: str | None = Form(default=None, alias="status"),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
box = Box(
|
|
|
|
|
name=name.strip(),
|
|
|
|
|
note=_clean_text(note),
|
|
|
|
|
room=_clean_text(room),
|
|
|
|
|
status=_clean_text(status_text),
|
2026-04-19 12:13:07 +02:00
|
|
|
)
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(box, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.add(box)
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(box)
|
|
|
|
|
return RedirectResponse(url=f"/boxes/{box.id}", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
|
|
|
|
@app.get("/boxes/{box_id}")
|
|
|
|
|
def show_box(box_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="boxes/show.html",
|
|
|
|
|
context={"page_title": box.name, "box": box},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.get("/boxes/{box_id}/image")
|
|
|
|
|
def get_box_image(box_id: int, db: Session = Depends(get_db)) -> Response:
|
|
|
|
|
return _image_response_or_404(_get_box_or_404(db, box_id))
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.get("/boxes/{box_id}/edit")
|
|
|
|
|
def edit_box_page(box_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="boxes/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": f"编辑箱子:{box.name}",
|
|
|
|
|
"box": box,
|
|
|
|
|
"form_action": f"/boxes/{box.id}/update",
|
|
|
|
|
"submit_label": "保存箱子",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/boxes/{box_id}/update")
|
|
|
|
|
def update_box(
|
|
|
|
|
box_id: int,
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
room: str | None = Form(default=None),
|
|
|
|
|
status_text: str | None = Form(default=None, alias="status"),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
box.name = name.strip()
|
|
|
|
|
box.note = _clean_text(note)
|
|
|
|
|
box.room = _clean_text(room)
|
|
|
|
|
box.status = _clean_text(status_text)
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(box, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/boxes/{box.id}", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.post("/boxes/{box_id}/image/delete")
|
|
|
|
|
def delete_box_image(box_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
_clear_image_fields(box)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/boxes/{box.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.post("/boxes/{box_id}/delete")
|
|
|
|
|
def delete_box(box_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
db.delete(box)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url="/boxes", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
|
|
|
|
@app.get("/boxes/{box_id}/items/new")
|
|
|
|
|
def new_item_page(box_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="items/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": f"为箱子“{box.name}”添加物品",
|
|
|
|
|
"box": box,
|
|
|
|
|
"item": None,
|
|
|
|
|
"form_action": f"/boxes/{box.id}/items",
|
|
|
|
|
"submit_label": "创建物品",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/boxes/{box_id}/items")
|
|
|
|
|
def create_item(
|
|
|
|
|
box_id: int,
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
quantity: str | None = Form(default=None),
|
|
|
|
|
is_container: str | None = Form(default=None),
|
2026-04-19 13:31:17 +02:00
|
|
|
submit_action: str | None = Form(default=None),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
box = _get_box_or_404(db, box_id)
|
|
|
|
|
item = Item(
|
|
|
|
|
box=box,
|
|
|
|
|
name=name.strip(),
|
|
|
|
|
note=_clean_text(note),
|
|
|
|
|
quantity=_parse_quantity(quantity),
|
|
|
|
|
is_container=_is_checked(is_container),
|
|
|
|
|
)
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(item, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.add(item)
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(item)
|
2026-04-19 13:31:17 +02:00
|
|
|
if _wants_add_next(submit_action):
|
|
|
|
|
redirect_url = f"/boxes/{box.id}/items/new"
|
|
|
|
|
elif item.is_container:
|
|
|
|
|
redirect_url = f"/items/{item.id}"
|
|
|
|
|
else:
|
|
|
|
|
redirect_url = f"/boxes/{box.id}"
|
|
|
|
|
return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER)
|
2026-04-19 12:36:55 +02:00
|
|
|
|
|
|
|
|
@app.get("/items/{item_id}")
|
|
|
|
|
def show_item(item_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="items/show.html",
|
|
|
|
|
context={"page_title": item.name, "item": item},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.get("/items/{item_id}/image")
|
|
|
|
|
def get_item_image(item_id: int, db: Session = Depends(get_db)) -> Response:
|
|
|
|
|
return _image_response_or_404(_get_item_or_404(db, item_id))
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.get("/items/{item_id}/edit")
|
|
|
|
|
def edit_item_page(item_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="items/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": f"编辑物品:{item.name}",
|
|
|
|
|
"box": item.box,
|
|
|
|
|
"item": item,
|
|
|
|
|
"form_action": f"/items/{item.id}/update",
|
|
|
|
|
"submit_label": "保存物品",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/items/{item_id}/update")
|
|
|
|
|
def update_item(
|
|
|
|
|
item_id: int,
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
quantity: str | None = Form(default=None),
|
|
|
|
|
is_container: str | None = Form(default=None),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
item.name = name.strip()
|
|
|
|
|
item.note = _clean_text(note)
|
|
|
|
|
item.quantity = _parse_quantity(quantity)
|
|
|
|
|
item.is_container = _is_checked(is_container)
|
|
|
|
|
if not item.is_container:
|
|
|
|
|
item.subitems.clear()
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(item, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.post("/items/{item_id}/image/delete")
|
|
|
|
|
def delete_item_image(item_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
_clear_image_fields(item)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/items/{item.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.post("/items/{item_id}/delete")
|
|
|
|
|
def delete_item(item_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
box_id = item.box_id
|
|
|
|
|
db.delete(item)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/boxes/{box_id}", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
|
|
|
|
@app.get("/items/{item_id}/subitems/new")
|
|
|
|
|
def new_subitem_page(item_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
_require_container_item(item)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="subitems/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": f"为“{item.name}”添加子物品",
|
|
|
|
|
"item": item,
|
|
|
|
|
"subitem": None,
|
|
|
|
|
"form_action": f"/items/{item.id}/subitems",
|
|
|
|
|
"submit_label": "创建子物品",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/items/{item_id}/subitems")
|
|
|
|
|
def create_subitem(
|
|
|
|
|
item_id: int,
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
quantity: str | None = Form(default=None),
|
2026-04-19 13:31:17 +02:00
|
|
|
submit_action: str | None = Form(default=None),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
item = _get_item_or_404(db, item_id)
|
|
|
|
|
_require_container_item(item)
|
|
|
|
|
subitem = SubItem(
|
|
|
|
|
parent_item=item,
|
|
|
|
|
name=name.strip(),
|
|
|
|
|
note=_clean_text(note),
|
|
|
|
|
quantity=_parse_quantity(quantity),
|
|
|
|
|
)
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(subitem, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.add(subitem)
|
|
|
|
|
db.commit()
|
|
|
|
|
db.refresh(subitem)
|
2026-04-19 13:31:17 +02:00
|
|
|
if _wants_add_next(submit_action):
|
|
|
|
|
redirect_url = f"/items/{item.id}/subitems/new"
|
|
|
|
|
else:
|
|
|
|
|
redirect_url = f"/items/{item.id}"
|
|
|
|
|
return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER)
|
2026-04-19 12:36:55 +02:00
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.get("/subitems/{subitem_id}/image")
|
|
|
|
|
def get_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> Response:
|
|
|
|
|
return _image_response_or_404(_get_subitem_or_404(db, subitem_id))
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.get("/subitems/{subitem_id}/edit")
|
|
|
|
|
def edit_subitem_page(subitem_id: int, request: Request, db: Session = Depends(get_db)):
|
|
|
|
|
subitem = _get_subitem_or_404(db, subitem_id)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request=request,
|
|
|
|
|
name="subitems/form.html",
|
|
|
|
|
context={
|
|
|
|
|
"page_title": f"编辑子物品:{subitem.name}",
|
|
|
|
|
"item": subitem.parent_item,
|
|
|
|
|
"subitem": subitem,
|
|
|
|
|
"form_action": f"/subitems/{subitem.id}/update",
|
|
|
|
|
"submit_label": "保存子物品",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.post("/subitems/{subitem_id}/update")
|
|
|
|
|
def update_subitem(
|
|
|
|
|
subitem_id: int,
|
|
|
|
|
name: str = Form(...),
|
|
|
|
|
note: str | None = Form(default=None),
|
|
|
|
|
quantity: str | None = Form(default=None),
|
2026-04-19 12:54:25 +02:00
|
|
|
image_file: UploadFile | None = File(default=None),
|
2026-04-19 12:36:55 +02:00
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> RedirectResponse:
|
|
|
|
|
subitem = _get_subitem_or_404(db, subitem_id)
|
|
|
|
|
subitem.name = name.strip()
|
|
|
|
|
subitem.note = _clean_text(note)
|
|
|
|
|
subitem.quantity = _parse_quantity(quantity)
|
2026-04-19 12:54:25 +02:00
|
|
|
_set_image_fields(subitem, process_upload(image_file))
|
2026-04-19 12:36:55 +02:00
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(
|
|
|
|
|
url=f"/items/{subitem.parent_item_id}",
|
|
|
|
|
status_code=status.HTTP_303_SEE_OTHER,
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 12:54:25 +02:00
|
|
|
@app.post("/subitems/{subitem_id}/image/delete")
|
|
|
|
|
def delete_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
subitem = _get_subitem_or_404(db, subitem_id)
|
|
|
|
|
_clear_image_fields(subitem)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/subitems/{subitem.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
@app.post("/subitems/{subitem_id}/delete")
|
|
|
|
|
def delete_subitem(subitem_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
|
|
|
|
subitem = _get_subitem_or_404(db, subitem_id)
|
|
|
|
|
item_id = subitem.parent_item_id
|
|
|
|
|
db.delete(subitem)
|
|
|
|
|
db.commit()
|
|
|
|
|
return RedirectResponse(url=f"/items/{item_id}", status_code=status.HTTP_303_SEE_OTHER)
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = create_app()
|