step 2 with basic crud implemented
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.engine import make_url
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
from app.config import get_settings
|
||||
@@ -14,8 +15,31 @@ class Base(DeclarativeBase):
|
||||
|
||||
|
||||
def _build_engine(database_url: str):
|
||||
_ensure_sqlite_directory(database_url)
|
||||
connect_args = {"check_same_thread": False} if database_url.startswith("sqlite") else {}
|
||||
return create_engine(database_url, connect_args=connect_args)
|
||||
created_engine = create_engine(database_url, connect_args=connect_args)
|
||||
|
||||
if database_url.startswith("sqlite"):
|
||||
@event.listens_for(created_engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
return created_engine
|
||||
|
||||
|
||||
def _ensure_sqlite_directory(database_url: str) -> None:
|
||||
if not database_url.startswith("sqlite"):
|
||||
return
|
||||
|
||||
database_path = make_url(database_url).database
|
||||
if not database_path or database_path == ":memory:":
|
||||
return
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
Path(database_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def configure_database(database_url: str | None = None) -> None:
|
||||
@@ -23,6 +47,8 @@ def configure_database(database_url: str | None = None) -> None:
|
||||
|
||||
settings = get_settings()
|
||||
resolved_database_url = database_url or settings.database_url
|
||||
if engine is not None:
|
||||
engine.dispose()
|
||||
engine = _build_engine(resolved_database_url)
|
||||
SessionLocal.configure(bind=engine)
|
||||
|
||||
|
||||
+299
-7
@@ -1,36 +1,328 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import Depends, FastAPI, Form, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import init_db
|
||||
from app.db import get_db, init_db
|
||||
from app.models import Box, Item, SubItem
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
init_db()
|
||||
yield
|
||||
|
||||
app = FastAPI(title="Moving Helper", lifespan=lifespan)
|
||||
app = FastAPI(title="搬家助手", lifespan=lifespan)
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
def root() -> RedirectResponse:
|
||||
return RedirectResponse(url="/boxes", status_code=302)
|
||||
return RedirectResponse(url="/boxes", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
@app.get("/boxes")
|
||||
def boxes_page(request: Request):
|
||||
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.html",
|
||||
context={"page_title": "Boxes"},
|
||||
name="boxes/index.html",
|
||||
context={"page_title": "箱子", "boxes": boxes},
|
||||
)
|
||||
|
||||
@app.get("/boxes/new")
|
||||
def new_box_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
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"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
box = Box(
|
||||
name=name.strip(),
|
||||
note=_clean_text(note),
|
||||
room=_clean_text(room),
|
||||
status=_clean_text(status_text),
|
||||
)
|
||||
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},
|
||||
)
|
||||
|
||||
@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"),
|
||||
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)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/boxes/{box.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@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),
|
||||
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),
|
||||
)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@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},
|
||||
)
|
||||
|
||||
@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),
|
||||
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()
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@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),
|
||||
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),
|
||||
)
|
||||
db.add(subitem)
|
||||
db.commit()
|
||||
db.refresh(subitem)
|
||||
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@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),
|
||||
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)
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
url=f"/items/{subitem.parent_item_id}",
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
@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)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
+70
-5
@@ -1,15 +1,80 @@
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db import Base
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
class Box(Base):
|
||||
__tablename__ = "boxes"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, default="Sample Box")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
room: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=utcnow,
|
||||
onupdate=utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
items: Mapped[list["Item"]] = relationship(
|
||||
back_populates="box",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="Item.id",
|
||||
)
|
||||
|
||||
|
||||
class Item(Base):
|
||||
__tablename__ = "items"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
box_id: Mapped[int] = mapped_column(ForeignKey("boxes.id", ondelete="CASCADE"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
quantity: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
is_container: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=utcnow,
|
||||
onupdate=utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
box: Mapped[Box] = relationship(back_populates="items")
|
||||
subitems: Mapped[list["SubItem"]] = relationship(
|
||||
back_populates="parent_item",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="SubItem.id",
|
||||
)
|
||||
|
||||
|
||||
class SubItem(Base):
|
||||
__tablename__ = "subitems"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
parent_item_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("items.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
quantity: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=utcnow,
|
||||
onupdate=utcnow,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
parent_item: Mapped[Item] = relationship(back_populates="subitems")
|
||||
|
||||
+106
-1
@@ -3,10 +3,11 @@ body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f4f4f4;
|
||||
color: #222;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
max-width: 840px;
|
||||
margin: 48px auto;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
@@ -18,3 +19,107 @@ h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0b57d0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
background: #0b57d0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
button:hover {
|
||||
opacity: 0.92;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.meta,
|
||||
.muted {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.checkbox-row input {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #b42318;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ page_title or "Moving Helper" }}</title>
|
||||
<title>{{ page_title or "搬家助手" }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<nav class="top-nav">
|
||||
<a href="/boxes">箱子</a>
|
||||
</nav>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Boxes page</h1>
|
||||
<p>This is the minimal starter page for the boxes module.</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ page_title }}</h1>
|
||||
<a href="/boxes">返回箱子列表</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ box.name if box else '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
房间
|
||||
<input type="text" name="room" value="{{ box.room if box and box.room else '' }}">
|
||||
</label>
|
||||
<label>
|
||||
状态
|
||||
<input type="text" name="status" value="{{ box.status if box and box.status else '' }}">
|
||||
</label>
|
||||
<label>
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ box.note if box and box.note else '' }}</textarea>
|
||||
</label>
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>箱子</h1>
|
||||
<p class="muted">这里管理顶层搬家容器,例如纸箱、行李箱或大收纳箱。</p>
|
||||
</div>
|
||||
<a class="button" href="/boxes/new">新建箱子</a>
|
||||
</div>
|
||||
|
||||
{% if boxes %}
|
||||
<div class="stack">
|
||||
{% for box in boxes %}
|
||||
<section class="card">
|
||||
<h2><a href="/boxes/{{ box.id }}">{{ box.name }}</a></h2>
|
||||
<p class="meta">物品数:{{ box.items|length }}</p>
|
||||
{% if box.room %}<p>房间:{{ box.room }}</p>{% endif %}
|
||||
{% if box.status %}<p>状态:{{ box.status }}</p>{% endif %}
|
||||
{% if box.note %}<p>{{ box.note }}</p>{% endif %}
|
||||
<div class="actions">
|
||||
<a href="/boxes/{{ box.id }}">查看详情</a>
|
||||
<a href="/boxes/{{ box.id }}/edit">编辑</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<p>还没有箱子。</p>
|
||||
<a href="/boxes/new">创建第一个箱子</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ box.name }}</h1>
|
||||
<p class="muted">查看这个箱子的基本信息,以及它下面的直接物品。</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/boxes">返回箱子列表</a>
|
||||
<a class="button" href="/boxes/{{ box.id }}/items/new">添加物品</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<p><strong>房间:</strong> {{ box.room or '-' }}</p>
|
||||
<p><strong>状态:</strong> {{ box.status or '-' }}</p>
|
||||
<p><strong>备注:</strong> {{ box.note or '-' }}</p>
|
||||
<div class="actions">
|
||||
<a href="/boxes/{{ box.id }}/edit">编辑箱子</a>
|
||||
<form method="post" action="/boxes/{{ box.id }}/delete">
|
||||
<button type="submit" class="link-button">删除箱子</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<h2>物品</h2>
|
||||
{% if box.items %}
|
||||
{% for item in box.items %}
|
||||
<article class="card">
|
||||
<h3><a href="/items/{{ item.id }}">{{ item.name }}</a></h3>
|
||||
<p><strong>是否容器:</strong> {{ "是" if item.is_container else "否" }}</p>
|
||||
{% if item.quantity is not none %}<p><strong>数量:</strong> {{ item.quantity }}</p>{% endif %}
|
||||
{% if item.note %}<p><strong>备注:</strong> {{ item.note }}</p>{% endif %}
|
||||
<div class="actions">
|
||||
<a href="/items/{{ item.id }}">查看详情</a>
|
||||
<a href="/items/{{ item.id }}/edit">编辑</a>
|
||||
{% if item.is_container %}
|
||||
<a href="/items/{{ item.id }}">查看内部内容</a>
|
||||
{% endif %}
|
||||
<form method="post" action="/items/{{ item.id }}/delete">
|
||||
<button type="submit" class="link-button">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<p>这个箱子里还没有物品。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ page_title }}</h1>
|
||||
<p class="muted">所属箱子:<a href="/boxes/{{ box.id }}">{{ box.name }}</a></p>
|
||||
</div>
|
||||
<a href="/boxes/{{ box.id }}">返回箱子</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ item.name if item else '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ item.quantity if item and item.quantity is not none else '' }}">
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="is_container" {% if item and item.is_container %}checked{% endif %}>
|
||||
这个物品本身是一个小容器
|
||||
</label>
|
||||
<label>
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ item.note if item and item.note else '' }}</textarea>
|
||||
</label>
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ item.name }}</h1>
|
||||
<p class="muted">位于箱子 <a href="/boxes/{{ item.box.id }}">{{ item.box.name }}</a> 中</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/boxes/{{ item.box.id }}">返回箱子</a>
|
||||
<a href="/items/{{ item.id }}/edit">编辑物品</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<p><strong>是否容器:</strong> {{ "是" if item.is_container else "否" }}</p>
|
||||
<p><strong>数量:</strong> {{ item.quantity if item.quantity is not none else '-' }}</p>
|
||||
<p><strong>备注:</strong> {{ item.note or '-' }}</p>
|
||||
<div class="actions">
|
||||
<form method="post" action="/items/{{ item.id }}/delete">
|
||||
<button type="submit" class="link-button">删除物品</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if item.is_container %}
|
||||
<section class="stack">
|
||||
<div class="page-header">
|
||||
<h2>子物品</h2>
|
||||
<a class="button" href="/items/{{ item.id }}/subitems/new">添加子物品</a>
|
||||
</div>
|
||||
{% if item.subitems %}
|
||||
{% for subitem in item.subitems %}
|
||||
<article class="card">
|
||||
<h3>{{ subitem.name }}</h3>
|
||||
{% if subitem.quantity is not none %}<p><strong>数量:</strong> {{ subitem.quantity }}</p>{% endif %}
|
||||
{% if subitem.note %}<p><strong>备注:</strong> {{ subitem.note }}</p>{% endif %}
|
||||
<div class="actions">
|
||||
<a href="/subitems/{{ subitem.id }}/edit">编辑</a>
|
||||
<form method="post" action="/subitems/{{ subitem.id }}/delete">
|
||||
<button type="submit" class="link-button">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<p>还没有子物品。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="card">
|
||||
<p>这个物品不是容器,因此不能包含子物品。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ page_title }}</h1>
|
||||
<p class="muted">上级物品:<a href="/items/{{ item.id }}">{{ item.name }}</a></p>
|
||||
</div>
|
||||
<a href="/items/{{ item.id }}">返回物品</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ subitem.name if subitem else '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ subitem.quantity if subitem and subitem.quantity is not none else '' }}">
|
||||
</label>
|
||||
<label>
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ subitem.note if subitem and subitem.note else '' }}</textarea>
|
||||
</label>
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user